ProntoID API Documentation
Welcome to the ProntoID API documentation. Our API provides a simple and secure way to integrate age verification and other platform utilities into your application. We offer several integration methods, including a direct Age Verification API, a Secure File Upload Flow, and a simplified OIDC Login flow.
Authentication
ProntoID uses two primary methods for authenticating server-to-server API requests. Please use the correct method for the endpoint you are calling.
Header Authentication
Used for Age Verification API
👉 Use this method for:
/prod/verification-start
x-api-key: Your secret API Keyx-platform-id: Your unique Platform ID
Body Authentication
Used for Platform Utilities
👉 Use this method for:
/production/get-platform-operation-authorization-session-token
platform_client_id: Your Platform IDplatform_api_key: Your Client Secret
⚡ Rate Limits
All endpoints are rate limited to 100 requests per minute per API key. If you exceed this limit, you'll receive a 429 Too Many Requests response.
Common Issues & Troubleshooting
Here are solutions to common problems you might encounter when integrating with ProntoID.
❌ Invalid Signature Error
If you're getting signature verification errors, ensure you're using the raw request body, not parsed JSON.
// ❌ Wrong - Don't use parsed data
$payload = json_encode($_POST);
// ✅ Correct - Use raw input stream
$payload = file_get_contents('php://input');
🔌 Testing Webhooks Locally
To test webhooks on your local development machine, use a tunneling service like ngrok:
ngrok http 3000
Then use the generated HTTPS URL (e.g., https://abc123.ngrok.io/webhook) in your ProntoID dashboard.
⏱️ Token Expiration
Session tokens expire after 10 minutes. If users see an "expired token" error:
- Generate a new token from your backend
- Don't store tokens - generate them on-demand
- Redirect users immediately after token generation
🔑 Authentication Errors
If you receive a 401 Unauthorized error:
- Verify you're using the correct authentication method for the endpoint
- Check that your credentials are not expired
- Ensure there are no extra spaces in your API keys
- Confirm you're using the right environment (production vs. sandbox)
Age Verification for Authenticated Users
This flow is designed for users who are already signed into your platform. The process is asynchronous: you initiate the verification, the user completes the steps on ProntoID, and we notify your server of the result via a secure webhook.
Step 1: Initiate the Verification
To begin, make a server-to-server POST request to our verification-start endpoint using Header Authentication. This creates a new age verification session and returns a URL to redirect your user to.
/prod/verification-start
Base URL: https://7b6fsp0mpi.execute-api.us-east-1.amazonaws.com
JSON Body Parameters
| client_reference | Required. A unique and stable identifier for the user from your system (e.g., your internal user ID). This value will be returned in the webhook payload, allowing you to easily look up and update the correct user record. |
Success Response (200 OK)
{
"verification_url": "https://secure.prontoid.com/verify?token=vtok_abc123...",
"verification_token": "vtok_abc123...",
"expires_at": "2024-11-07T16:30:00Z"
}
Error Response (401 Unauthorized)
{
"error": "unauthorized",
"error_description": "Invalid API key or Platform ID"
}
Step 2: Receive the Webhook Notification
After the user completes the verification process, ProntoID will send an asynchronous event to your configured webhook endpoint. You can set up your endpoint URL and subscribe to events like age_verification.succeeded in your ProntoID developer dashboard.
Your Endpoint Must Be Secure
Your server must verify the webhook's signature to ensure it originated from ProntoID and was not tampered with. This is a critical security step.
Step 3: Verify the Signature
Every webhook request from ProntoID includes an X-Pronto-Signature header. This signature is a HMAC-SHA256 hash of the raw request body, created using your unique webhook signing secret (whsec_...).
<?php
// Get the secret from an environment variable for security
$webhook_secret = $_ENV['PRONTO_WEBHOOK_SECRET'];
// Get the signature from the request headers
$received_signature = $_SERVER['HTTP_X_PRONTO_SIGNATURE'] ?? '';
// Get the raw POST data from the request body
$payload_body = file_get_contents('php://input');
// Compute the expected signature
$expected_signature = hash_hmac('sha256', $payload_body, $webhook_secret);
// Use hash_equals for a timing-attack-safe comparison
if (!hash_equals($expected_signature, $received_signature)) {
// Signature is invalid. Reject the request.
http_response_code(403);
exit('Invalid signature.');
}
// Signature is valid. Proceed to process the event.
$event = json_decode($payload_body, true);
// ... your logic here ...
import os
import hmac
import hashlib
# In a web framework like Flask, you would get these from the request context
# from flask import request
# Get the secret from an environment variable for security
webhook_secret = os.environ.get('PRONTO_WEBHOOK_SECRET')
# Get the signature from the request headers
received_signature = request.headers.get('X-Pronto-Signature')
# Get the raw POST data from the request body
payload_body = request.data
# Compute the expected signature
expected_signature = hmac.new(
webhook_secret.encode('utf-8'),
payload_body,
hashlib.sha256
).hexdigest()
# Use hmac.compare_digest for a timing-attack-safe comparison
if not hmac.compare_digest(expected_signature, received_signature):
# Signature is invalid. Reject the request.
return 'Invalid signature.', 403
# Signature is valid. Proceed to process the event.
event = request.get_json()
# ... your logic here ...
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
// In a web framework, get these from the request context
// String receivedSignature = request.headers("X-Pronto-Signature");
// String payloadBody = request.body();
// Get the secret from an environment variable for security
String webhookSecret = System.getenv("PRONTO_WEBHOOK_SECRET");
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] hash = sha256_HMAC.doFinal(payloadBody.getBytes(StandardCharsets.UTF_8));
String expectedSignature = HexFormat.of().formatHex(hash);
// Use MessageDigest.isEqual for a timing-attack-safe comparison
if (!MessageDigest.isEqual(expectedSignature.getBytes(), receivedSignature.getBytes())) {
// Signature is invalid. Reject the request.
// response.status(403);
// return "Invalid signature.";
}
// Signature is valid. Proceed to process the event.
// ... your logic here ...
} catch (Exception e) {
// Handle exceptions
// response.status(500);
// return "Server error.";
}
Step 4: Process the Event Data
Once the signature is verified, you can safely process the event payload. The type field tells you which event occurred, and the full details are in the data.object.
Example Payload for age_verification.succeeded
{
"id": "evt_1a2b3c4d5e6f7g8h...",
"object": "event",
"api_version": "1.0",
"created": 1728735780,
"type": "age_verification.succeeded",
"data": {
"object": {
"platform_id": "your_platform_id",
"verification_token": "vtok_abcdef123456",
"is_adult": true,
"age_status": "verified_adult",
"client_reference": "your_internal_user_id_12345"
}
}
}
After receiving this event, you should use the client_reference to look up the user in your database and update their status to reflect that they are now age-verified.
Login with ProntoID (OIDC Flow)
For a robust integration, use our OpenID Connect (OIDC) flow. Our helper endpoints simplify the process, handling the complexities of token generation and validation. Authentication for this flow uses a Client ID and Client Secret which you can get from your ProntoID dashboard.
Primary Use Cases
1. Secure Age Verification
The primary use case is to confirm that a user has successfully completed an age verification process. After a successful login, the decoded ID Token will contain the https://prontoid.com/age_verified claim, which will be set to true.
2. User Login & Account Linking
You can also use this flow as a standard "Login with ProntoID" feature. The ID Token includes a stable and unique user identifier in the sub claim. You can use this ID to sign users into their existing accounts on your platform or to provision a new account for them.
Step 1: Get the Authorization URL
To begin, make a GET request to our authorization URL helper endpoint from your server. This endpoint returns a complete, secure URL containing a unique state parameter. You must parse this state value from the returned URL and store it in a secure, server-side session or an HttpOnly cookie before redirecting the user.
/production/pronto-login-get-authorization-uri
Base URL: https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com
Query String Parameters
| client_id | Required. Your unique Client ID. |
| scope |
Optional. A space-separated string of scopes to request specific claims. If omitted, your platform's default scope will be used.
|
Example: Requesting the Auth URL with Scopes
<?php
$API_ENDPOINT = 'https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/pronto-login-get-authorization-uri';
$CLIENT_ID = getenv('PRONTOID_CLIENT_ID');
// For both user ID and age verification
$params = http_build_query([
'client_id' => $CLIENT_ID,
'scope' => 'openid age_verification'
]);
$url = $API_ENDPOINT . '?' . $params;
// ... cURL GET request to $url to fetch the full authorization_uri ...
import os
import requests
API_ENDPOINT = "https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/pronto-login-get-authorization-uri"
params = {
"client_id": os.environ.get("PRONTOID_CLIENT_ID"),
"scope": "openid age_verification" # Request both claims
}
response = requests.get(API_ENDPOINT, params=params)
response.raise_for_status()
auth_uri = response.json().get("authorization_uri")
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
String apiEndpoint = "https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/pronto-login-get-authorization-uri";
String clientId = System.getenv("PRONTOID_CLIENT_ID");
// For both user ID and age verification
String scope = "openid age_verification";
String query = String.format("client_id=%s&scope=%s",
URLEncoder.encode(clientId, StandardCharsets.UTF_8),
URLEncoder.encode(scope, StandardCharsets.UTF_8)
);
String fullUrl = apiEndpoint + "?" + query;
// ... Perform HttpClient GET request to fullUrl ...
Step 2: Get the ID Token
After the user signs in, they are redirected to your redirect_uri with an authorization_code. Verify the state parameter, then exchange the code for an ID Token by making a secure server-to-server POST request to our token endpoint.
/production/get-oidc-token
Base URL: https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com
JSON Body Parameters
| client_id | Required. Your unique Client ID. |
| client_secret | Required. Your Client Secret. |
| authorization_code | Required. The code received in the callback. |
Example: Exchanging the Code for a Token
<?php
$TOKEN_ENDPOINT = 'https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/get-oidc-token';
$CLIENT_ID = getenv('PRONTOID_CLIENT_ID');
$CLIENT_SECRET = getenv('PRONTOID_CLIENT_SECRET');
$authorization_code = $_GET['code'];
// state verification logic...
$payload = json_encode([
'client_id' => $CLIENT_ID,
'client_secret' => $CLIENT_SECRET,
'authorization_code' => $authorization_code
]);
// ... cURL POST request to $TOKEN_ENDPOINT with $payload ...
$responseBody = curl_exec($ch);
$body = json_decode($responseBody, true);
$id_token = $body['id_token'];
import os
import requests
# ... state verification logic ...
TOKEN_ENDPOINT = "https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/get-oidc-token"
payload = {
"client_id": os.environ.get("PRONTOID_CLIENT_ID"),
"client_secret": os.environ.get("PRONTOID_CLIENT_SECRET"),
"authorization_code": request.args.get('code'),
}
response = requests.post(TOKEN_ENDPOINT, json=payload)
response.raise_for_status()
id_token = response.json().get("id_token")
// ... state verification logic ...
String tokenEndpoint = "https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/get-oidc-token";
String clientId = System.getenv("PRONTOID_CLIENT_ID");
String clientSecret = System.getenv("PRONTOID_CLIENT_SECRET");
String authorizationCode = request.getParameter("code");
String payload = String.format(
"{\"client_id\": \"%s\", \"client_secret\": \"%s\", \"authorization_code\": \"%s\"}",
clientId, clientSecret, authorizationCode
);
// ... HttpClient POST request to tokenEndpoint with payload ...
// Parse response.body() to get the "id_token"
Step 3: Verify the ID Token
For maximum security and simplicity, you should not validate the JWT manually. Instead, send the id_token you received to our dedicated verification endpoint. This endpoint will perform all necessary cryptographic and claim validations on your behalf and return the token's claims if it is valid.
/production/verify-oidc-token
Base URL: https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com
JSON Body Parameters
| id_token | Required. The raw ID Token string you received in Step 2. |
| client_id | Required. Your unique Client ID. |
| client_secret | Required. Your Client Secret, used to authenticate this request. |
Example: Verifying the Token
<?php
$VERIFY_ENDPOINT = 'https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/verify-oidc-token';
$id_token = '... the token from Step 2 ...';
$payload = json_encode([
'id_token' => $id_token,
'client_id' => getenv('PRONTOID_CLIENT_ID'),
'client_secret' => getenv('PRONTOID_CLIENT_SECRET')
]);
// ... cURL POST request to $VERIFY_ENDPOINT with $payload ...
$responseBody = curl_exec($ch);
$token_data = json_decode($responseBody, true);
if ($token_data['active'] === true) {
// Token is valid, access claims like $token_data['sub']
$is_age_verified = $token_data['https://prontoid.com/age_verified'] ?? false;
}
import os
import requests
VERIFY_ENDPOINT = "https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/verify-oidc-token"
id_token = "... the token from Step 2 ..."
payload = {
"id_token": id_token,
"client_id": os.environ.get("PRONTOID_CLIENT_ID"),
"client_secret": os.environ.get("PRONTOID_CLIENT_SECRET"),
}
response = requests.post(VERIFY_ENDPOINT, json=payload)
response.raise_for_status()
token_data = response.json()
if token_data.get("active"):
# Token is valid, access claims like token_data.get("sub")
is_age_verified = token_data.get("https://prontoid.com/age_verified")
String verifyEndpoint = "https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/verify-oidc-token";
String idToken = "... the token from Step 2 ...";
String payload = String.format(
"{\"id_token\": \"%s\", \"client_id\": \"%s\", \"client_secret\": \"%s\"}",
idToken, System.getenv("PRONTOID_CLIENT_ID"), System.getenv("PRONTOID_CLIENT_SECRET")
);
// ... HttpClient POST request to verifyEndpoint with payload ...
// Parse response JSON into a Map or custom object
// Map tokenData = ...
if (Boolean.TRUE.equals(tokenData.get("active"))) {
// Token is valid, access claims
boolean isAgeVerified = (boolean) tokenData.get("https://prontoid.com/age_verified");
}
Example Success Responses
The claims returned in a successful response depend on the scope you requested in Step 1.
Response for scope=openid age_verification
{
"active": true,
"iss": "https://prontoid.com",
"sub": "user-uuid-12345",
"aud": "your_client_id_here",
"exp": 1728681125,
"iat": 1728677525,
"https://prontoid.com/age_verified": true
}
Response for scope=openid
{
"active": true,
"iss": "https://prontoid.com",
"sub": "user-uuid-12345",
"aud": "your_client_id_here",
"exp": 1728681125,
"iat": 1728677525
}
Secure File Upload Flow (Model Release)
This flow allows your users to securely upload a file (like a Model Release Form) from your platform, have it processed and stored by ProntoID, and then have the resulting form_id automatically associated with your content.
This process uses a secure, single-use JWT token for authentication and a signed webhook for confirmation. It is a three-step asynchronous process.
Step 1: Get a One-Time Session Token
When a user wants to upload a file, your backend server must call our session token endpoint. This is a secure server-to-server call using Body Authentication (see Authentication).
This endpoint will return a short-lived (e.g., 10 minutes) JSON Web Token (JWT). This token is safe to send to your frontend JavaScript.
/production/get-platform-operation-authorization-session-token
Base URL: https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com
JSON Body Parameters
| platform_client_id | Required. Your unique Platform ID (e.g., bentbox_4b07...). |
| platform_api_key | Required. Your Platform's Client Secret (e.g., c_d3oXn...). |
| platform_content_url | Required. The full URL of the content (photo, video, etc.) this file will be associated with. This will be shown to the user on the upload page. |
| platform_callback_url | Required. The URL on your platform where the user should be redirected after a successful upload. |
| action_type | Required. Must be set to RELEASE_FORM_UPLOAD. |
Example: Getting the Session Token
<?php
// This code runs on your server (e.g., BentBox)
$PRONTOID_CLIENT_ID = getenv('PRONTOID_CLIENT_ID');
$PRONTOID_CLIENT_SECRET = getenv('PRONTOID_CLIENT_SECRET');
$PRONTOID_API_ENDPOINT = 'https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/get-platform-operation-authorization-session-token';
// Data from your authenticated user
$content_url_to_verify = 'https://your-platform.com/content/abc-123';
$final_callback_url = 'https://your-platform.com/pronto_return';
$payload = json_encode([
'platform_client_id' => $PRONTOID_CLIENT_ID,
'platform_api_key' => $PRONTOID_CLIENT_SECRET,
'platform_content_url' => $content_url_to_verify,
'platform_callback_url' => $final_callback_url,
'expiration_seconds' => 600,
'action_type' => 'RELEASE_FORM_UPLOAD'
]);
// ... cURL POST request to $PRONTOID_API_ENDPOINT with $payload ...
$response_json = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code == 200) {
$response_data = json_decode($response_json, true);
$session_token = $response_data['session_token'];
// Securely return this token to your frontend JavaScript
echo json_encode(['status' => 'success', 'session_token' => $session_token]);
} else {
// Handle error
echo json_encode(['error' => 'Could not initiate session.']);
}
import os
import requests
PRONTOID_CLIENT_ID = os.environ.get("PRONTOID_CLIENT_ID")
PRONTOID_CLIENT_SECRET = os.environ.get("PRONTOID_CLIENT_SECRET")
PRONTOID_API_ENDPOINT = "https://uxvq4ew6td.execute-api.us-east-1.amazonaws.com/production/get-platform-operation-authorization-session-token"
# Data from your authenticated user
content_url_to_verify = "https://your-platform.com/content/abc-123"
final_callback_url = "https://your-platform.com/pronto_return"
payload = {
"platform_client_id": PRONTOID_CLIENT_ID,
"platform_api_key": PRONTOID_CLIENT_SECRET,
"platform_content_url": content_url_to_verify,
"platform_callback_url": final_callback_url,
"expiration_seconds": 600,
"action_type": "RELEASE_FORM_UPLOAD"
}
response = requests.post(PRONTOID_API_ENDPOINT, json=payload)
response.raise_for_status() # Raises exception for non-2xx status
session_token = response.json().get("session_token")
# Securely return this token to your frontend JavaScript
# (e.g., in a Flask response)
# return jsonify({"status": "success", "session_token": session_token})
Step 2: Redirect the User to ProntoID
Once your frontend JavaScript receives the one-time session_token from your backend, it must immediately redirect the user to the ProntoID secure upload page. The token is passed as a URL query parameter.
Example: JavaScript Frontend
// This code runs in the user's browser on your platform (e.g., BentBox)
async function onUploadButtonClick() {
// 1. Get the one-time token from your backend API
const response = await fetch('/your-backend/get-pronto-token-api', {
method: 'POST',
body: JSON.stringify({
content_id: 'abc-123', // Your internal content ID
// ... other params ...
})
});
const data = await response.json();
if (data.status === 'success') {
// 2. Build the secure URL
const sessionToken = data.session_token;
const prontoUrl = `https://secure.prontoid.com/secure-upload-model-release-form.php?token=${encodeURIComponent(sessionToken)}`;
// 3. Redirect the user (e.g., in a new tab)
window.open(prontoUrl, '_blank');
} else {
alert('Error: ' + data.error);
}
}
When this page loads, ProntoID will consume the token from the URL, validate it, and exchange it for a secure, HttpOnly first-party cookie (prontoid_secure_session). The user then completes the form, and all subsequent API calls from that page (S3 uploads, form save) are authenticated with this new, secure cookie.
Step 3: Receive the Confirmation Webhook
After the user successfully saves the form, two things happen:
- The user's browser is redirected back to the
platform_callback_urlyou provided in Step 1. - ProntoID's backend asynchronously sends a signed webhook to your platform's registered
webhook_urlto confirm the save.
You must listen for this webhook, verify its signature (using the same logic as in Step 3 of the Age Verification flow), and then use the payload to link the form_id to your content.
Example Payload for release_form.completed
{
"id": "evt_2a3b4c5d6e7f8g9h...",
"object": "event",
"api_version": "1.0",
"created": 1728745780,
"type": "release_form.completed",
"data": {
"object": {
"platform_id": "bentbox_4b07...",
"form_id": "ac416eb28c5f2e3ac5cb4af619c2f115",
"content_url": "https://bentbox.co/box/box_12345abc",
"status": "completed"
}
}
}
After verifying the webhook signature, use the content_url and form_id to update your database and finalize the association.