curl --request POST \
--url https://vitarelay.com/api/public/v1/patients/{id}/payment-methods \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"opaque_data": {
"dataDescriptor": "<string>",
"dataValue": "<string>"
},
"billing": {
"first_name": "<string>",
"last_name": "<string>",
"line1": "<string>",
"city": "<string>",
"state": "<string>",
"postal_code": "<string>",
"country": "<string>"
},
"nickname": "<string>",
"make_default": true
}
'import requests
url = "https://vitarelay.com/api/public/v1/patients/{id}/payment-methods"
payload = {
"opaque_data": {
"dataDescriptor": "<string>",
"dataValue": "<string>"
},
"billing": {
"first_name": "<string>",
"last_name": "<string>",
"line1": "<string>",
"city": "<string>",
"state": "<string>",
"postal_code": "<string>",
"country": "<string>"
},
"nickname": "<string>",
"make_default": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
opaque_data: {dataDescriptor: '<string>', dataValue: '<string>'},
billing: {
first_name: '<string>',
last_name: '<string>',
line1: '<string>',
city: '<string>',
state: '<string>',
postal_code: '<string>',
country: '<string>'
},
nickname: '<string>',
make_default: true
})
};
fetch('https://vitarelay.com/api/public/v1/patients/{id}/payment-methods', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://vitarelay.com/api/public/v1/patients/{id}/payment-methods",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'opaque_data' => [
'dataDescriptor' => '<string>',
'dataValue' => '<string>'
],
'billing' => [
'first_name' => '<string>',
'last_name' => '<string>',
'line1' => '<string>',
'city' => '<string>',
'state' => '<string>',
'postal_code' => '<string>',
'country' => '<string>'
],
'nickname' => '<string>',
'make_default' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://vitarelay.com/api/public/v1/patients/{id}/payment-methods"
payload := strings.NewReader("{\n \"opaque_data\": {\n \"dataDescriptor\": \"<string>\",\n \"dataValue\": \"<string>\"\n },\n \"billing\": {\n \"first_name\": \"<string>\",\n \"last_name\": \"<string>\",\n \"line1\": \"<string>\",\n \"city\": \"<string>\",\n \"state\": \"<string>\",\n \"postal_code\": \"<string>\",\n \"country\": \"<string>\"\n },\n \"nickname\": \"<string>\",\n \"make_default\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://vitarelay.com/api/public/v1/patients/{id}/payment-methods")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"opaque_data\": {\n \"dataDescriptor\": \"<string>\",\n \"dataValue\": \"<string>\"\n },\n \"billing\": {\n \"first_name\": \"<string>\",\n \"last_name\": \"<string>\",\n \"line1\": \"<string>\",\n \"city\": \"<string>\",\n \"state\": \"<string>\",\n \"postal_code\": \"<string>\",\n \"country\": \"<string>\"\n },\n \"nickname\": \"<string>\",\n \"make_default\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://vitarelay.com/api/public/v1/patients/{id}/payment-methods")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"opaque_data\": {\n \"dataDescriptor\": \"<string>\",\n \"dataValue\": \"<string>\"\n },\n \"billing\": {\n \"first_name\": \"<string>\",\n \"last_name\": \"<string>\",\n \"line1\": \"<string>\",\n \"city\": \"<string>\",\n \"state\": \"<string>\",\n \"postal_code\": \"<string>\",\n \"country\": \"<string>\"\n },\n \"nickname\": \"<string>\",\n \"make_default\": true\n}"
response = http.request(request)
puts response.read_body{
"data": {
"id": "8c1d5a0e-2b7f-4e3a-9d41-6f0a1b2c3d4e",
"brand": "Visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2030,
"nickname": null,
"is_default": true,
"created_at": "2026-10-02T15:04:11.220Z"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}Save a patient's card (no charge)
Vault a patient’s card without charging it, so a subscription can be created
for a patient who has never bought anything. Requires orders:write or
patients:write.
- On your own page, load Accept.js (
checkout.accept_js_urlfrom anypatient_checkoutorder, or your Accept.js keys) and callAccept.dispatchDatato tokenize the card in the browser. Card data never touches your server or VitaRelay’s. - POST the single-use nonce here as
opaque_data. - Use the returned
idaspayment_method_idwhen creating subscription lines onPOST /orders.
Billing details default to the patient record. The card belongs to the patient.
With a test key the response is a simulated sandbox_pm_… card.
curl --request POST \
--url https://vitarelay.com/api/public/v1/patients/{id}/payment-methods \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"opaque_data": {
"dataDescriptor": "<string>",
"dataValue": "<string>"
},
"billing": {
"first_name": "<string>",
"last_name": "<string>",
"line1": "<string>",
"city": "<string>",
"state": "<string>",
"postal_code": "<string>",
"country": "<string>"
},
"nickname": "<string>",
"make_default": true
}
'import requests
url = "https://vitarelay.com/api/public/v1/patients/{id}/payment-methods"
payload = {
"opaque_data": {
"dataDescriptor": "<string>",
"dataValue": "<string>"
},
"billing": {
"first_name": "<string>",
"last_name": "<string>",
"line1": "<string>",
"city": "<string>",
"state": "<string>",
"postal_code": "<string>",
"country": "<string>"
},
"nickname": "<string>",
"make_default": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
opaque_data: {dataDescriptor: '<string>', dataValue: '<string>'},
billing: {
first_name: '<string>',
last_name: '<string>',
line1: '<string>',
city: '<string>',
state: '<string>',
postal_code: '<string>',
country: '<string>'
},
nickname: '<string>',
make_default: true
})
};
fetch('https://vitarelay.com/api/public/v1/patients/{id}/payment-methods', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://vitarelay.com/api/public/v1/patients/{id}/payment-methods",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'opaque_data' => [
'dataDescriptor' => '<string>',
'dataValue' => '<string>'
],
'billing' => [
'first_name' => '<string>',
'last_name' => '<string>',
'line1' => '<string>',
'city' => '<string>',
'state' => '<string>',
'postal_code' => '<string>',
'country' => '<string>'
],
'nickname' => '<string>',
'make_default' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://vitarelay.com/api/public/v1/patients/{id}/payment-methods"
payload := strings.NewReader("{\n \"opaque_data\": {\n \"dataDescriptor\": \"<string>\",\n \"dataValue\": \"<string>\"\n },\n \"billing\": {\n \"first_name\": \"<string>\",\n \"last_name\": \"<string>\",\n \"line1\": \"<string>\",\n \"city\": \"<string>\",\n \"state\": \"<string>\",\n \"postal_code\": \"<string>\",\n \"country\": \"<string>\"\n },\n \"nickname\": \"<string>\",\n \"make_default\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://vitarelay.com/api/public/v1/patients/{id}/payment-methods")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"opaque_data\": {\n \"dataDescriptor\": \"<string>\",\n \"dataValue\": \"<string>\"\n },\n \"billing\": {\n \"first_name\": \"<string>\",\n \"last_name\": \"<string>\",\n \"line1\": \"<string>\",\n \"city\": \"<string>\",\n \"state\": \"<string>\",\n \"postal_code\": \"<string>\",\n \"country\": \"<string>\"\n },\n \"nickname\": \"<string>\",\n \"make_default\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://vitarelay.com/api/public/v1/patients/{id}/payment-methods")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"opaque_data\": {\n \"dataDescriptor\": \"<string>\",\n \"dataValue\": \"<string>\"\n },\n \"billing\": {\n \"first_name\": \"<string>\",\n \"last_name\": \"<string>\",\n \"line1\": \"<string>\",\n \"city\": \"<string>\",\n \"state\": \"<string>\",\n \"postal_code\": \"<string>\",\n \"country\": \"<string>\"\n },\n \"nickname\": \"<string>\",\n \"make_default\": true\n}"
response = http.request(request)
puts response.read_body{
"data": {
"id": "8c1d5a0e-2b7f-4e3a-9d41-6f0a1b2c3d4e",
"brand": "Visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2030,
"nickname": null,
"is_default": true,
"created_at": "2026-10-02T15:04:11.220Z"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid"
}
}Authorizations
API key issued by VitaRelay. Send as Authorization: Bearer vr_live_…
(production) or Authorization: Bearer vr_test_… (sandbox).
Path Parameters
The VitaRelay patient id (UUID), or your own external_patient_id from when you created the patient — you never have to store our id.
Body
Show child attributes
Show child attributes
Cardholder billing details. Anything omitted falls back to the patient record.
Show child attributes
Show child attributes
Display-only label (max 40 characters).
Make this the patient's default card. It already is when they have none.
Response
Card saved.
Show child attributes
Show child attributes
{
"id": "8c1d5a0e-2b7f-4e3a-9d41-6f0a1b2c3d4e",
"brand": "Visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2030,
"nickname": null,
"is_default": true,
"created_at": "2026-10-02T15:04:11.220Z"
}

