API Fundamentals
Essential concepts and conventions for working with the Paysera Delivery API.
Base URL
All API requests should be made to:
https://delivery-api.paysera.com/rest/v1/
Delivery API is currently available only in Lithuania (LT), Latvia (LV), and Estonia (EE).
REST Principles
The Paysera API follows REST principles using HTTP verbs:
- GET - Retrieve resources
- POST - Create new resources
- PUT - Update entire resource
- DELETE - Remove resources
Request bodies in POST/PUT operations use JSON format with UTF-8 encoding. All responses return JSON-encoded data.
Response Handling
HTTP status code 200 is returned for successful requests.
Optional elements in responses are omitted entirely rather than returned as null values. This reduces payload size and simplifies parsing.
Authentication
The API uses MAC (Message Authentication Code) access authentication based on the OAuth 2.0 specification.
Credential Mapping
Map your Paysera credentials to MAC authentication parameters:
project_id→ MAC identifier (mac_id)sign_password→ MAC secret (mac_secret)
These credentials are obtained from your Project Settings in the Paysera dashboard (see Obtaining Credentials).
MAC Header Format
Authorization: MAC id="123456",
ts="1700574800",
nonce="n123abc",
mac="computed_signature_here"
MAC Authentication Parameters
| Parameter | Description | Required |
|---|---|---|
id | Your project_id | ✅ Yes |
ts | Current Unix timestamp in seconds | ✅ Yes |
nonce | Unique random string for this request | ✅ Yes |
mac | HMAC-SHA256 signature of the request | ✅ Yes |
Generating MAC Signature
The MAC signature is computed as follows:
-
Create the signature base string:
{timestamp}\n{nonce}\n{http_method}\n{request_uri}\n{host}\n{port}\n\n -
Compute HMAC-SHA256:
mac = Base64(HMAC-SHA256(mac_secret, signature_base_string))
Example MAC Authentication Implementation
- JavaScript
- PHP
- Python
const crypto = require('crypto');
function generateMacAuth(method, uri, host, port, projectId, signPassword) {
const timestamp = Math.floor(Date.now() / 1000);
const nonce = crypto.randomBytes(8).toString('hex');
// Create signature base string
const baseString = [
timestamp,
nonce,
method.toUpperCase(),
uri,
host,
port,
'',
''
].join('\n');
// Compute HMAC-SHA256 signature
const mac = crypto
.createHmac('sha256', signPassword)
.update(baseString)
.digest('base64');
// Return Authorization header value
return `MAC id="${projectId}", ts="${timestamp}", nonce="${nonce}", mac="${mac}"`;
}
// Usage example
const authHeader = generateMacAuth(
'POST',
'/rest/v1/orders',
'delivery-api.paysera.com',
443,
'123456', // your project_id
'your_sign_password' // your sign_password
);
console.log('Authorization:', authHeader);
<?php
function generateMacAuth($method, $uri, $host, $port, $projectId, $signPassword) {
$timestamp = time();
$nonce = bin2hex(random_bytes(8));
// Create signature base string
$baseString = implode("\n", [
$timestamp,
$nonce,
strtoupper($method),
$uri,
$host,
$port,
'',
''
]);
// Compute HMAC-SHA256 signature
$mac = base64_encode(hash_hmac('sha256', $baseString, $signPassword, true));
// Return Authorization header value
return sprintf(
'MAC id="%s", ts="%s", nonce="%s", mac="%s"',
$projectId,
$timestamp,
$nonce,
$mac
);
}
// Usage example
$authHeader = generateMacAuth(
'POST',
'/rest/v1/orders',
'delivery-api.paysera.com',
443,
'123456', // your project_id
'your_sign_password' // your sign_password
);
echo 'Authorization: ' . $authHeader;
?>
import hmac
import hashlib
import base64
import time
import secrets
def generate_mac_auth(method, uri, host, port, project_id, sign_password):
timestamp = str(int(time.time()))
nonce = secrets.token_hex(8)
# Create signature base string
base_string = '\n'.join([
timestamp,
nonce,
method.upper(),
uri,
host,
str(port),
'',
''
])
# Compute HMAC-SHA256 signature
mac = base64.b64encode(
hmac.new(
sign_password.encode('utf-8'),
base_string.encode('utf-8'),
hashlib.sha256
).digest()
).decode('utf-8')
# Return Authorization header value
return f'MAC id="{project_id}", ts="{timestamp}", nonce="{nonce}", mac="{mac}"'
# Usage example
auth_header = generate_mac_auth(
'POST',
'/rest/v1/orders',
'delivery-api.paysera.com',
443,
'123456', # your project_id
'your_sign_password' # your sign_password
)
print('Authorization:', auth_header)
- Keep your
sign_passwordsecure and never expose it in client-side code - Store credentials as environment variables in your application
- Generate a new unique nonce for each request
- The timestamp must be current (servers may reject requests with timestamps too far in the past or future)
REST Principles
The API follows REST architectural principles:
- Uses standard HTTP verbs (
GET,POST,PUT,DELETE) - Resource-based URLs
- Stateless operations
- JSON for data exchange
Request Format
- Headers
- Request Body
- Query Parameters
Required Headers
All requests must include these headers:
Authorization: MAC id="123456", ts="1700574800", nonce="n123abc", mac="signature_here"
Content-Type: application/json; charset=utf-8
Accept: application/json
Optional Headers
X-Request-ID: unique-request-id-123
Accept-Language: en
User-Agent: YourApp/1.0
All requests must use UTF-8 encoding for proper handling of international characters
JSON Format
All POST and PUT requests must send data as JSON:
{
"order_id": "ORD-12345",
"courier": "dpd",
"pickup": {
"name": "Sender Name",
"address": "Street 1",
"city": "Vilnius",
"postal_code": "01001",
"country": "LT",
"phone": "+37060000001"
},
"delivery": {
"name": "Recipient Name",
"address": "Street 2",
"city": "Kaunas",
"postal_code": "44001",
"country": "LT",
"phone": "+37060000002"
},
"parcel": {
"weight": 1.5,
"dimensions": {
"length": 30,
"width": 20,
"height": 15
}
}
}
Data Types
| Field Type | Format | Example |
|---|---|---|
| Strings | UTF-8 encoded | "John Doe" |
| Numbers | Decimal | 1.5, 30 |
| Dates | ISO 8601 | "2025-11-21T14:30:00Z" |
| Phone | E.164 format | "+37060000001" |
| Country | ISO 3166-1 alpha-2 | "LT", "LV", "EE" |
| Currency | Amount in EUR | 49.99 |
Pagination
For endpoints that return lists:
GET /orders?page=1&limit=20
| Parameter | Default | Max | Description |
|---|---|---|---|
page | 1 | - | Page number |
limit | 20 | 100 | Items per page |
sort | -created_at | - | Sort field and direction |
Filtering
GET /orders?status=delivered&from=2025-11-01&to=2025-11-30
Common filters:
status- Order statuscourier- Courier codefrom- Start date (ISO 8601)to- End date (ISO 8601)tracking_number- Package tracking number
Response Format
Optional elements in responses are omitted entirely rather than returned as null values. This reduces payload size and simplifies parsing.
Successful Response
Single Resource
{
"id": "DEL-789456",
"order_id": "ORD-12345",
"status": "in_transit",
"tracking_number": "PS123456789LT",
"courier": "dpd",
"created_at": "2025-11-21T10:00:00Z",
"updated_at": "2025-11-21T14:30:00Z",
"estimated_delivery": "2025-11-22T18:00:00Z",
"pickup": {
"name": "Sender Name",
"address": "Street 1",
"city": "Vilnius",
"postal_code": "01001",
"country": "LT"
},
"delivery": {
"name": "Recipient Name",
"address": "Street 2",
"city": "Kaunas",
"postal_code": "44001",
"country": "LT"
}
}
List Response
{
"data": [
{
"id": "DEL-789456",
"order_id": "ORD-12345",
"status": "delivered"
},
{
"id": "DEL-789457",
"order_id": "ORD-12346",
"status": "in_transit"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"pages": 8
}
}
Error Response
Error Structure
Error responses contain three possible fields:
{
"error": "invalid_request",
"error_description": "The request contains invalid data",
"error_uri": "https://developers.paysera.com/errors/invalid_request"
}
Error Response Fields
| Field | Required | Description |
|---|---|---|
error | ✅ Yes | Required code identifying the error type |
error_description | ❌ No | Optional human-readable explanation |
error_uri | ❌ No | Optional reference link to error documentation |
HTTP Status Codes
Common HTTP status codes returned by the API:
| Code | Meaning | Common Causes |
|---|---|---|
| 200 | Success | Request processed successfully |
| 400 | Bad Request | Invalid request format or malformed parameters |
| 401 | Unauthorized | Authentication failed or missing credentials |
| 403 | Forbidden | Valid credentials but insufficient permissions |
| 404 | Not Found | Resource doesn't exist or endpoint not found |
| 409 | Conflict | Invalid state transition for resource |
| 500 | Internal Server Error | Server-side error, temporary issue |
HTTP Methods
| Method | Usage | Idempotent |
|---|---|---|
| GET | Retrieve resources | ✅ Yes |
| POST | Create new resources | ❌ No |
| PUT | Update entire resource | ✅ Yes |
| PATCH | Update partial resource | ❌ No |
| DELETE | Remove resources | ✅ Yes |
Method Examples
- GET
- POST
- PUT
- DELETE
# Get single order
GET /orders/DEL-789456
# List orders
GET /orders
# Track package
GET /track/PS123456789LT
# Get order events
GET /orders/DEL-789456/events
# Create order
POST /orders
{
"order_id": "ORD-12345",
"courier": "dpd",
...
}
# Cancel order
POST /orders/DEL-789456/cancel
{
"reason": "customer_request"
}
# Create return
POST /returns
{
"original_order_id": "DEL-789456",
...
}
# Update delivery address
PUT /orders/DEL-789456/delivery
{
"name": "New Recipient",
"address": "New Street 10",
"city": "Vilnius",
"postal_code": "01002",
"country": "LT",
"phone": "+37060000003"
}
# Delete draft order
DELETE /orders/DEL-789456
# Remove webhook
DELETE /webhooks/hook_123
HTTP Status Codes
Always check the HTTP status code first to determine whether the response contains success data or an error object. Success responses return 200 OK for most operations.
Success Codes
| Code | Meaning | Usage |
|---|---|---|
| 200 | OK | Request succeeded, data returned |
| 201 | Created | Resource created successfully |
| 204 | No Content | Request succeeded, no content to return |
Client Error Codes
| Code | Meaning | Common Causes |
|---|---|---|
| 400 | Bad Request | Invalid request format, malformed JSON |
| 401 | Unauthorized | Missing or invalid authentication credentials |
| 403 | Forbidden | Valid credentials but insufficient permissions |
| 404 | Not Found | Resource doesn't exist or wrong endpoint |
| 409 | Conflict | Invalid state transition or duplicate resource |
| 422 | Unprocessable Entity | Request valid but contains semantic errors |
| 429 | Too Many Requests | Rate limit exceeded |
Server Error Codes
| Code | Meaning | Action |
|---|---|---|
| 500 | Internal Server Error | Temporary issue, retry with exponential backoff |
| 502 | Bad Gateway | Upstream service issue, retry later |
| 503 | Service Unavailable | Service maintenance or overload |
| 504 | Gateway Timeout | Request processing timeout, retry |
Idempotency
Using Idempotency Keys
Prevent duplicate operations by including an idempotency key:
POST /orders
Idempotency-Key: unique-key-123
Content-Type: application/json
{
"order_id": "ORD-12345",
...
}
Key Guidelines
- Generate unique keys for each distinct operation
- Reuse the same key when retrying failed requests
- Keys are valid for 24 hours after first use
- Use UUIDs or similar unique identifiers
Example Implementation
const { v4: uuidv4 } = require('uuid');
async function createOrderWithIdempotency(orderData) {
const idempotencyKey = uuidv4();
// Generate MAC authentication header (see authentication section above)
const authHeader = generateMacAuth('POST', '/rest/v1/orders', 'delivery-api.paysera.com', 443, projectId, signPassword);
const response = await fetch('https://delivery-api.paysera.com/rest/v1/orders', {
method: 'POST',
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(orderData)
});
// If request fails, retry with same idempotency key
if (!response.ok && response.status >= 500) {
return retryWithSameKey(idempotencyKey, orderData);
}
return response.json();
}
Webhooks
Webhook Configuration
Webhook Payload
{
"id": "evt_123456",
"type": "delivery.status_changed",
"created_at": "2025-11-21T14:30:00Z",
"data": {
"id": "DEL-789456",
"order_id": "ORD-12345",
"old_status": "in_transit",
"new_status": "delivered",
"tracking_number": "PS123456789LT"
}
}
Webhook Headers
POST /your-webhook-endpoint
Content-Type: application/json
X-Paysera-Signature: sha256=abc123...
X-Paysera-Event: delivery.status_changed
X-Paysera-Delivery-ID: evt_123456
Event Types
| Event | Description |
|---|---|
delivery.created | New delivery order created |
delivery.status_changed | Delivery status updated |
delivery.delivered | Package delivered successfully |
delivery.failed | Delivery attempt failed |
delivery.returned | Package returned to sender |
delivery.cancelled | Order cancelled |
Signature Verification
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return `sha256=${expectedSignature}` === signature;
}
Rate Limiting
Understanding Rate Limits
Rate Limit Headers
Every response includes rate limit information:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 85
X-RateLimit-Reset: 1700574800
Limits by Tier
| Tier | Requests/min | Requests/hour | Daily |
|---|---|---|---|
| Standard | 100 | 3,000 | 50,000 |
| Business | 300 | 10,000 | 200,000 |
| Enterprise | Custom | Custom | Custom |
Handling Rate Limits
async function handleRateLimitedRequest(requestFn) {
const response = await requestFn();
if (response.status === 429) {
const resetTime = response.headers.get('X-RateLimit-Reset');
const waitTime = (resetTime * 1000) - Date.now();
console.log(`Rate limited. Waiting ${waitTime}ms`);
await new Promise(resolve => setTimeout(resolve, waitTime));
// Retry the request
return requestFn();
}
return response;
}
Best Practices
- Cache responses when possible
- Batch operations to reduce API calls
- Implement exponential backoff for retries
- Monitor rate limit headers proactively
- Use webhooks instead of polling
Data Formats
Standard Formats
Address Format
{
"name": "John Doe",
"company": "Optional Company Name",
"address": "Street Name 123",
"address2": "Apartment 4B",
"city": "Vilnius",
"state": "Optional State/Province",
"postal_code": "01001",
"country": "LT",
"phone": "+37060000001",
"email": "john@example.com"
}
Parcel Specifications
{
"weight": 2.5, // kg
"dimensions": {
"length": 30, // cm
"width": 20, // cm
"height": 15 // cm
},
"value": 99.99, // EUR
"description": "Electronics",
"fragile": true,
"insurance": true,
"reference": "SKU-12345"
}
Date/Time Format
All dates use ISO 8601 format with UTC timezone:
2025-11-21T14:30:00Z
For date ranges:
{
"from": "2025-11-01T00:00:00Z",
"to": "2025-11-30T23:59:59Z"
}
Versioning
The API uses URL versioning:
https://delivery-api.paysera.com/rest/v1/...
Version Policy
- Current version: v1
- Deprecation notice: 6 months minimum
- Sunset period: 12 months after deprecation
- Breaking changes: New version only
- Non-breaking changes: Added to current version
Checking API Version
curl -X GET 'https://delivery-api.paysera.com/version' \
-H 'Authorization: MAC id="123456", ts="1700574800", nonce="abc123", mac="..."'
Response:
{
"version": "v1",
"released": "2024-01-01",
"deprecated": false,
"sunset_date": null
}
Testing
Testing Strategy
Sample Test
describe('Delivery API', () => {
test('should create order with valid data', async () => {
const order = await api.createOrder(validOrderData);
expect(order).toHaveProperty('id');
expect(order).toHaveProperty('tracking_number');
expect(order.status).toBe('created');
});
test('should handle validation errors', async () => {
const invalidData = { ...validOrderData, phone: '123' };
await expect(api.createOrder(invalidData))
.rejects.toThrow('validation_error');
});
test('should retry on server errors', async () => {
// Mock server error then success
mockAPI.onFirstCall().returns(500);
mockAPI.onSecondCall().returns(200);
const order = await api.createOrder(validOrderData);
expect(order).toBeDefined();
expect(mockAPI.callCount).toBe(2);
});
});
Support
Need help with complex integrations?
Contact: tech_support@paysera.com