This walkthrough validates authentication, catalog access, file upload, and result delivery in sandbox.
This quickstart uses the source-IP-allowlist path. Obtain the sandbox API token, confirm the calling IP is allowlisted, and configure a public HTTPS webhook endpoint with Percents. To use mTLS instead, complete the mTLS quickstart and use https://mtls.test.percents.com as the base URL.
export PERCENTS_API_BASE='https://sandbox.percents.com'
export PERCENTS_API_TOKEN='tok_your_id:api_your_secret'curl --fail-with-body \
--header "Authorization: token ${PERCENTS_API_TOKEN}" \
"${PERCENTS_API_BASE}/api/v1/omni/merchant"
curl --fail-with-body \
--header "Authorization: token ${PERCENTS_API_TOKEN}" \
"${PERCENTS_API_BASE}/api/v1/omni/offer"Persist merchant and offer UUIDs exactly as returned. Do not infer access to a merchant from its name.
curl --fail-with-body --get \
--header "Authorization: token ${PERCENTS_API_TOKEN}" \
--data-urlencode 'content=percents-txn-stream' \
--data-urlencode 'fileType=csv' \
--data-urlencode 'fileName=transactions-2026-10-01-001.csv' \
"${PERCENTS_API_BASE}/api/fileUpload"The response contains a presigned URL that expires after 15 minutes:
{
"url": "https://object-storage.example/presigned-upload",
"fileId": "fu_77777777-7777-4777-8777-777777777777",
"expiresAt": "2026-10-01T12:15:00.000Z",
"headers": {}
}Create the CSV using the native row contract. Send every header returned in the upload response; Content-Type is optional for native CSV. Use a unique filename for each reservation, including retries after a failed or expired file.
curl --fail-with-body \
--request PUT \
--data-binary @transactions.csv \
'PRESIGNED_URL_FROM_THE_PREVIOUS_RESPONSE'Do not send the Percents API token to the presigned URL. The URL itself grants temporary permission to upload that single object.
Return a successful 2xx response after validating the webhook signature and durably recording the event. Process each webhookId idempotently.
Wait for the signed FileUploadProcessingResult webhook or poll GET /api/v1/omni/files/{fileId} with your API token. Successful sandbox validation reports processingStatus=validation_only and validationStatus=passed. Failed validation reports processingStatus=failed and validationStatus=failed. Production ingests valid files automatically and reports ingestion completion/failure. Neither result is final approval of individual transactions, rewards, or invoices.