What your customer sees
Everything the customer sees is yours. Finrelay renders no UI; your backend encrypts the card details and sends them to the API.
{
"amount": 4990,
"currency": "EUR",
"payment_method": {
"type": "card",
"data": {
"encrypted_card_number": "base64_encrypted_value",
"encrypted_cvv": "base64_encrypted_value",
"encrypted_expiration_month": "base64_encrypted_value",
"encrypted_expiration_year": "base64_encrypted_value"
}
},
"return_url": "https://shop.example/return"
}
Illustrative preview with test data.
- Your card formYou build and host every field, including card number and CVV.
- Encrypted requestYour backend encrypts the card number, CVV and expiry with your merchant public key before sending them. Your integration must meet PCI DSS requirements.
- 3-D Secure and resultSend
browser_infoand areturn_urlfor 3-D Secure, then checkstatusandapprovedin the response.
Compare integrations
How the three integrations differ.
| Hosted Payment Page | Embedded Fields | API Integration | |
|---|---|---|---|
| Checkout runs on | Finrelay | Your page | Your page |
| PCI DSS scope | Lowest | Reduced | Full |
| Customization | Logo, colours, language | CSS variables | Full UI control |
| Effort | Lowest | Medium | Highest |
Integrate in four steps
Get an access token
Use the OIDC Client Credentials flow with your client ID and secret.
Encrypt the card data
Encrypt card number, CVV, expiration month and year with your merchant public key.
Authorize the payment
Send the encrypted card, browser_info and a return_url to /api/transactions/authorize.
Handle the result
Check status and approved, complete 3-D Secure when required, and use webhooks for status updates.
curl -X POST https://api.finrelay.io/api/transactions/authorize \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"reference": "ORDER-123456",
"terminal_id": "TERM001",
"description": "Order payment",
"amount": 4990,
"currency": "EUR",
"transaction_type": "PURCHASE",
"payment_method": { "type": "card", "data": { "encrypted_card_number": "..." } },
"browser_info": { "...": "..." },
"return_url": "https://shop.example/return"
}'
const res = await fetch('https://api.finrelay.io/api/transactions/authorize', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
reference: 'ORDER-123456',
terminal_id: 'TERM001',
description: 'Order payment',
amount: 4990, // minor units
currency: 'EUR',
transaction_type: 'PURCHASE',
payment_method: { type: 'card', data: encryptedCard },
browser_info: browserInfo,
return_url: 'https://shop.example/return',
}),
});
const result = await res.json();
API Integration
Direct API integration allows you to process payments programmatically using RESTful endpoints. This approach gives you full control over the payment flow and UI/UX.
Authentication
All API requests require a valid OAuth 2.0 access token:
HTTP Headers
Authorization: Bearer <access_token>
Content-Type: application/json
Obtaining an Access Token
Use the Client Credentials flow to obtain an access token. See Obtain Access Token for detailed instructions.
Your client_id and client_secret are available in your merchant dashboard.
Processing Card Payments
Endpoint
POST /api/transactions/authorize
Data Encryption
All sensitive card data must be encrypted using your merchant public key before sending to the API.
Fields requiring encryption: - Card number - CVV - Expiration month - Expiration year
See the Encryption Example for detailed encryption instructions.
Card Payment Data Fields
| Field | Type | Required | Encrypted | Description |
|---|---|---|---|---|
encrypted_card_number |
string |
Yes | Yes | Card PAN (digits only, no spaces) |
encrypted_cvv |
string |
Yes | Yes | Card security code (digits only) |
encrypted_expiration_month |
string |
Yes | Yes | Expiration month (1–12, no leading zero) |
encrypted_expiration_year |
string |
Yes | Yes | Expiration year — 2-digit (26) or 4-digit (2026); must be current or future year |
Basic Request Example
{
"reference": "ORDER-123456",
"terminal_id": "TERM001",
"description": "Order payment",
"currency": "EUR",
"amount": 10000,
"transaction_type": "PURCHASE",
"payment_method": {
"type": "card",
"data": {
"encrypted_card_number": "base64_encrypted_value",
"encrypted_cvv": "base64_encrypted_value",
"encrypted_expiration_month": "base64_encrypted_value",
"encrypted_expiration_year": "base64_encrypted_value"
}
},
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
"address": "123 Example Street",
"city": "Sampletown",
"country": "BA",
"postal_code": "12345"
},
"browser_info": {
"user_agent": "Mozilla/5.0...",
"accept_header": "text/html,application/xhtml+xml...",
"java_enabled": false,
"color_depth": 24,
"screen_height": 1080,
"screen_width": 1920,
"time_zone_offset": -120,
"language": "en-US"
},
"metadata": {},
"return_url": "https://merchant.example.com/return"
}
Network Token Payments (Apple Pay / Google Pay)
For merchants who handle wallet token decryption on their own infrastructure, the network-token payment method type allows you to send pre-decrypted network token data (DPAN, cryptogram, ECI) through the gateway.
This is useful for:
- Apple Pay integrations where you decrypt the PKPaymentToken server-side
- Google Pay integrations with CRYPTOGRAM_3DS or PAN_ONLY authentication
- Direct TSP (Token Service Provider) integrations
Data Encryption
The following fields must be encrypted using your merchant public key (same key used for card encryption):
- Token number (DPAN)
- Expiration month
- Expiration year
- Cryptogram (when present)
The eci and source fields are sent as plain text.
See the Encryption Example for encryption instructions.
Request Example (Apple Pay)
{
"reference": "ORDER-NT-001",
"terminal_id": "TERM001",
"description": "Apple Pay payment",
"currency": "EUR",
"amount": 10000,
"transaction_type": "PURCHASE",
"payment_method": {
"type": "network-token",
"data": {
"encrypted_token_number": "base64_encrypted_dpan",
"encrypted_expiration_month": "base64_encrypted_value",
"encrypted_expiration_year": "base64_encrypted_value",
"encrypted_cryptogram": "base64_encrypted_cryptogram",
"eci": "05",
"source": "apple-pay"
}
},
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
"address": "123 Example Street",
"city": "Sampletown",
"country": "BA",
"postal_code": "12345"
},
"browser_info": {
"user_agent": "Mozilla/5.0...",
"accept_header": "text/html,application/xhtml+xml...",
"java_enabled": false,
"color_depth": 24,
"screen_height": 1080,
"screen_width": 1920,
"time_zone_offset": -120,
"language": "en-US"
},
"metadata": {},
"return_url": "https://merchant.example.com/return"
}
Request Example (Google Pay — PAN_ONLY)
When using Google Pay with PAN_ONLY authentication, the cryptogram and ECI are not present:
{
"reference": "ORDER-NT-002",
"terminal_id": "TERM001",
"description": "Google Pay payment",
"currency": "EUR",
"amount": 5000,
"transaction_type": "PURCHASE",
"payment_method": {
"type": "network-token",
"data": {
"encrypted_token_number": "base64_encrypted_dpan",
"encrypted_expiration_month": "base64_encrypted_value",
"encrypted_expiration_year": "base64_encrypted_value",
"source": "google-pay"
}
},
"customer": { ... },
"browser_info": { ... },
"metadata": {},
"return_url": "https://merchant.example.com/return"
}
Network Token Payment Data Fields
| Field | Type | Required | Encrypted | Description |
|---|---|---|---|---|
encrypted_token_number |
string |
Yes | Yes | DPAN from the wallet or TSP |
encrypted_expiration_month |
string |
Yes | Yes | Token expiration month (1–12, no leading zero) |
encrypted_expiration_year |
string |
Yes | Yes | Token expiration year — 2-digit (26) or 4-digit (2026); must be current or future year |
encrypted_cryptogram |
string |
No | Yes | Payment cryptogram (absent for PAN_ONLY) |
eci |
string |
No | No | Electronic Commerce Indicator — required for CRYPTOGRAM_3DS, omitted for PAN_ONLY. Valid values: 00, 01, 02, 05, 06, 07 |
source |
string |
No | No | Token origin: apple-pay, google-pay, or network-token. Defaults to network-token if omitted |
For detailed information about network tokens, see the Network Token Reference.
Creating a Customer
Card tokenization associates a saved card with a customer, so the customer must exist before you save a card against it (see Saving Cards for Future Payments). Create one with the Customers API and reference the returned id as customer.id in your transactions.
Create a Customer
POST /api/customers
Request Example:
{
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"phone": "+38970123456",
"description": "Repeat customer",
"address": {
"country": "SI",
"address": "123 Main St",
"city": "Ljubljana",
"postal_code": "1000",
"region": "Osrednjeslovenska"
}
}
Request Fields:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Valid email address (max 512). The only required field. |
first_name |
string | No | Max 255. |
last_name |
string | No | Max 255. |
phone |
string | No | Contact phone number. |
description |
string | No | Free-text label (max 255). |
type |
string | No | Customer type. |
address |
object | No | Nested address object (fields below). |
address.country |
string | No | ISO 3166-1 alpha-2 country code (2 letters). |
address.address |
string | No | Street address (max 255). |
address.city |
string | No | Max 255. |
address.postal_code |
string | No | Max 255. |
address.region |
string | No | State/region (max 255). |
Response Example:
{
"id": "CUST123456",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+38970123456",
"address": "123 Main St",
"city": "Ljubljana",
"country": "SI",
"postal_code": "1000"
}
The returned id is the customer identifier — pass it as customer.id in subsequent authorization/purchase requests to link saved cards to this customer. The response flattens the address into top-level address, city, country, and postal_code.
Saving Cards for Future Payments
The API supports tokenization, allowing you to save payment cards for future transactions. This is useful for:
- Subscription or recurring payments
- One-click checkout experiences
- Repeat customer transactions
Requirements for Card Tokenization
Before using tokenization, ensure:
- Tokenization is enabled at the merchant level (contact your payment platform administrator)
- Customer exists (if using
customer.id): The customer must already exist in the Payment Platform — see Creating a Customer - PCI compliance: Ensure your integration complies with PCI-DSS requirements
Saving a Card During Payment
To save a card for future use, include the tokenization object in your transaction request:
Request Example:
{
"reference": "ORDER-123456",
"terminal_id": "TERM001",
"description": "Order payment",
"currency": "EUR",
"amount": 10000,
"transaction_type": "PURCHASE",
"payment_method": {
"type": "card",
"data": {
"encrypted_card_number": "base64_encrypted_value",
"encrypted_cvv": "base64_encrypted_value",
"encrypted_expiration_month": "base64_encrypted_value",
"encrypted_expiration_year": "base64_encrypted_value"
}
},
"customer": {
"id": "CUST123456",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
"address": "123 Example Street",
"city": "Sampletown",
"country": "BA",
"postal_code": "12345"
},
"tokenization": {
"save_card_for_future_payments": true
},
"browser_info": {
"user_agent": "Mozilla/5.0...",
"accept_header": "text/html,application/xhtml+xml...",
"java_enabled": false,
"color_depth": 24,
"screen_height": 1080,
"screen_width": 1920,
"time_zone_offset": -120,
"language": "en-US"
},
"metadata": {},
"return_url": "https://merchant.example.com/return"
}
Tokenization Parameters
| Field | Type | Description |
|---|---|---|
tokenization.save_card_for_future_payments |
boolean |
If true, automatically saves the card for future use |
tokenization.tokenize_for |
string |
Optional. Intended use of the saved payment method: CUSTOMER_INITIATED_TRANSACTION (default when omitted) or MERCHANT_INITIATED_TRANSACTION |
Compliance Note: When saving cards without explicit customer consent, ensure your terms of service and privacy policy inform customers that payment information will be stored for future use. Depending on your jurisdiction, this may have regulatory implications (GDPR, PCI-DSS, etc.).
Response with Saved Payment Method
When a card is successfully saved, the response includes a token in the payment_method object:
{
"result": {
"id": "TXN123456",
"status": "APPROVED",
"payment_method": {
"method": "411111-******-1111",
"type": "card",
"brand": "VISA",
"masked": "411111-******-1111",
"token": "abc123def456ghi789"
},
...
}
}
The token value (e.g., abc123def456ghi789) is the payment method ID that can be used for future transactions.
Retrieving the Token via Webhook
The synchronous response above returns the token in payment_method.token. If you rely on webhooks (for example, hosted-page or redirect flows), the token is delivered on a dedicated event — not on the transaction webhook:
Event: tokenization-webhook:payment-method:tokenized
{
"event": "tokenization-webhook:payment-method:tokenized",
"data": {
"payment_method_id": "abc123def456ghi789",
"payment_method_type": "CARD",
"card_brand": "VISA",
"card_bin": "411111",
"card_last_4": "1111",
"card_masked": "411111******1111",
"card_exp_month": 12,
"card_exp_year": 2028,
"customer_id": "CUST123456",
"transaction_id": "TXN123456"
}
}
Important: payment_method_token is null on the transaction-svc:transaction:purchase:approved webhook — this is expected, not an error. The token is delivered only on the tokenization-webhook:payment-method:tokenized event. To receive it, your webhook subscription must include that event (webhook subscriptions are configured by Finrelay — contact support to enable it).
Assigning Saved Payment Methods to a Customer
When saving a card, the customer.id field determines how the payment method is associated:
With customer.id (Recommended)
When you include customer.id in the request:
- The saved payment method is automatically assigned to the specified customer
- The customer must already exist in the Payment Platform
- The payment method can be retrieved and used in future transactions for this customer
- You can list all saved payment methods for a customer
Example:
{
"reference": "ORDER-123456",
"customer": {
"id": "CUST123456",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
},
"tokenization": {
"save_card_for_future_payments": true
},
...
}
Without customer.id
When you do not include customer.id in the request:
- The payment method is still saved and tokenized
- The payment method remains unassociated with any customer
- You can still use the payment method token for future transactions
- The payment method cannot be associated with a customer later
- You cannot retrieve this payment method through customer-based queries
Example:
{
"reference": "ORDER-123456",
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
},
"tokenization": {
"save_card_for_future_payments": true
},
...
}
Important Notes:
- Payment methods saved with a
customer.idare linked to that customer profile and can be managed through customer-based operations - Payment methods saved without a
customer.idremain permanently unassociated and can only be referenced by their token - Best practice: Always include
customer.idwhen saving cards to enable better customer management and payment method organization
Paying with Saved Cards
Once a card has been saved, you can use it for future transactions without requiring the customer to re-enter their card details.
Requirements
To pay with a saved card, you need:
- The payment method ID (token) from the original transaction response
- The CVV (encrypted) - may be required depending on Payment Provider support
Request Example
{
"reference": "ORDER-789012",
"terminal_id": "TERM001",
"description": "Subscription renewal",
"currency": "EUR",
"amount": 5000,
"transaction_type": "PURCHASE",
"payment_method": {
"type": "saved_card",
"data": {
"payment_method_id": "abc123def456ghi789",
"encrypted_cvv": "base64_encrypted_value"
}
},
"customer": {
"id": "CUST123456",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
},
"browser_info": {
"user_agent": "Mozilla/5.0...",
"accept_header": "text/html,application/xhtml+xml...",
"java_enabled": false,
"color_depth": 24,
"screen_height": 1080,
"screen_width": 1920,
"time_zone_offset": -120,
"language": "en-US"
},
"metadata": {},
"return_url": "https://merchant.example.com/return"
}
Key Differences for Saved Card Payments
| Field | Value | Description |
|---|---|---|
payment_method.type |
"saved_card" |
Indicates using a saved payment method |
payment_method.data.payment_method_id |
string |
The token from the original save operation |
payment_method.data.encrypted_cvv |
string |
Base64-encoded encrypted CVV |
Security Note:
- CVV may be required for saved card transactions depending on Payment Provider support
- When required, CVV must be encrypted using your merchant public key
- The full card number is never required for saved card payments
- Only the payment method ID (and CVV if required) are needed
Response Example
{
"result": {
"id": "TXN789012",
"status": "APPROVED",
"payment_method": {
"method": "411111-******-1111",
"type": "saved_card",
"brand": "VISA",
"masked": "411111-******-1111",
"token": "abc123def456ghi789"
},
"approved": true,
...
}
}
Deleting a Saved Card
A stored card can be removed at any time — when the customer asks for it, when they remove it themselves from a "saved cards" screen in your application, or when you retire it on your own.
Endpoint
DELETE /api/payment-methods/{payment_method_id}
The {payment_method_id} is the same token returned when the card was saved (the payment_method.token field of the transaction response, or payment_method_id on the tokenization-webhook:payment-method:tokenized event).
The request takes the same OAuth access token as the rest of the API, no request body, and — unlike saving a card — no Idempotency-Key header, because the call is already idempotent.
Your API client needs the payment_method:detach permission. If the call returns 403, contact support to have it enabled for your client.
Request Example
curl -X DELETE "{API_BASE_URL}/api/payment-methods/abc123def456ghi789" \
-H "Authorization: Bearer <access_token>"
See Environments for the API URL of your environment.
Response Example
200 OK, returning the payment method in its deleted state:
{
"id": "abc123def456ghi789",
"type": "CARD",
"status": "DELETED",
"customer_id": "",
"email": "john.doe@example.com",
"metadata": {},
"card": {
"card_brand": "VISA",
"exp_month": 12,
"exp_year": 2030,
"last_4": "1111",
"bin": "411111"
},
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-02-01T09:12:44Z"
}
What Deletion Does
- The card can no longer be charged. Any transaction sent with
payment_method.type: "saved_card"referencing a deleted payment method is rejected — see Charging a Deleted Card. - It is removed from the customer. The customer association is cleared, so the card no longer appears among the saved payment methods offered for that customer.
- The record is kept.
GET /api/payment-methods/{payment_method_id}keeps returning the payment method withstatus: "DELETED", so a card referenced by an older transaction can still be identified for reconciliation, disputes and audit. - Deletion is final. A deleted payment method cannot be reactivated or assigned to a customer again. To pay with the same card later, the customer has to enter it again and it is saved as a new payment method with a new id.
The stored card data itself remains in the vault, encrypted, for as long as dispute handling and regulatory obligations require — it is simply no longer reachable for payments. If a customer requires the card data to be erased permanently (for example a GDPR erasure request), contact support: that is handled as a separate operation.
Repeated Deletion
Deleting is idempotent. Repeating the call for a payment method that is already deleted returns the same 200 OK response and leaves the original deletion untouched, so a delete that timed out can safely be retried.
Unknown or Foreign Payment Method
A payment method id that does not exist — or that belongs to a different merchant — is reported as not found rather than as forbidden, so the response never reveals whether the id exists:
{
"status": "INVALID",
"errors": [
{
"message": "Not found"
}
]
}
Response Codes
| Status | Meaning |
|---|---|
200 |
The payment method is deleted (or was already deleted) |
403 |
The API client does not hold the payment_method:detach permission |
422 |
Not found — unknown id, or a payment method belonging to another merchant |
Implementing "Remove Card" in Your Application
The call uses your merchant API credentials, so it must be made from your backend. A "remove card" action in a mobile or web application should call your own backend, which then calls the Payment Gateway:
- The customer taps Remove card in your application.
- Your application calls your backend with the payment method id it is displaying.
- Your backend verifies that the payment method belongs to the signed-in customer, then calls
DELETE /api/payment-methods/{payment_method_id}. - On
200, your backend removes the card from the list it shows the customer.
Never perform this call directly from a client application: it would require your merchant API credentials to be present in the application. The publishable client key used by Embedded Fields and the Hosted Payment Page cannot delete payment methods.
Error Handling
Tokenization Not Enabled
If tokenization is not enabled at the merchant level:
{
"status": "INVALID",
"errors": [
{
"property": "tokenization",
"message": "one of requested features not supported=[TOKENIZATION]"
}
]
}
Solution: Contact your payment platform administrator to enable tokenization.
Customer Not Found
If you provide a customer.id that doesn't exist:
{
"status": "INVALID",
"errors": [
{
"property": "customer.id",
"message": "Customer with id 'CUST123456' not found"
}
]
}
Solution: Create the customer first, or omit customer.id to save the payment method without customer association.
Invalid Payment Method ID
If the payment method ID is invalid or doesn't exist:
{
"status": "INVALID",
"errors": [
{
"property": "payment_method.data.payment_method_id",
"message": "Payment method not found"
}
]
}
Solution: Verify the payment method ID is correct and that the payment method hasn't been deleted.
Charging a Deleted Card
If a transaction references a payment method that has been deleted (or otherwise deactivated), the transaction is rejected before it reaches the payment provider:
{
"status": "INVALID",
"errors": [
{
"property": "payment_method.data.payment_method_id",
"message": "INVALID_REQUEST_PAYMENT_METHOD_INACTIVE"
}
]
}
Solution: The card is no longer available for payments and cannot be restored. Ask the customer to enter the card again, which saves it as a new payment method. You can confirm the state of a payment method with GET /api/payment-methods/{payment_method_id} — a deleted one reports status: "DELETED".
Complete Workflow Examples
Workflow 1: First-Time Payment with Card Saving
- Encrypt card data using merchant public key
- Submit transaction with
tokenization.save_card_for_future_payments: true - Store payment method ID from response for future use
- Use saved card for subsequent payments
Workflow 2: Subscription Payment with Saved Card
- Retrieve payment method ID from your database (saved from first transaction)
- Encrypt CVV (collected from customer)
- Submit transaction with
payment_method.type: "saved_card" - Process response as normal transaction
Workflow 3: Customer Removes a Saved Card
- Customer requests removal of a card in your application
- Verify ownership in your backend — the payment method must belong to the signed-in customer
- Call
DELETE /api/payment-methods/{payment_method_id}with your merchant API credentials - Remove the card from the list shown to the customer on
200; the payment method stays retrievable withstatus: "DELETED"for your own reconciliation
Related Documentation
- Card Authorization API Reference - Detailed API specification
- Network Token Reference - Network token payment method details
- Apple Pay - Apple Pay overview and the available integration methods
- Encryption Example - How to encrypt card data
Best Practices
- Always encrypt sensitive data: Card numbers, CVV, and expiration dates must be encrypted
- Use customer IDs: Always associate saved cards with a customer ID for better organization
- Handle tokens securely: Store payment method IDs securely in your database
- Implement webhooks: Use webhooks for reliable payment status updates
- Validate responses: Always check the
statusandapprovedfields in responses - Error handling: Implement comprehensive error handling for all API calls
- Compliance: Ensure your integration meets PCI-DSS and local regulatory requirements