Guide
Quickstart: your first accepted batch
From the credentials Fence gives you to a first asset Fence has accepted, in five calls.
This guide takes you from your credentials to one asset that Fence holds. It takes five calls: get a token, find your asset program, send an asset, then read back the batch and the record.
What you need
Fence's onboarding team creates an integration for your system and gives you, once:
- a
client_idand aclient_secret. Fence does not keep the secret and cannot show it again, so store it in your secret manager straight away; - the token URL to ask for tokens at;
- the scopes the integration may use:
submissions:writeto send data,portfolio:readto read it.
The examples use these shell variables:
export FENCE_TOKEN_URL="<the token URL Fence gave you>"
export FENCE_API="<the API address Fence gave you>"
export CLIENT_ID="<your client_id>"
export CLIENT_SECRET="<your client_secret>"1. Get a token
Ask the token URL for a token with OAuth client credentials. Name the scopes with the fence-v3/ prefix.
TOKEN=$(curl -s -X POST "$FENCE_TOKEN_URL" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials \
-d scope="fence-v3/submissions:write fence-v3/portfolio:read" | jq -r .access_token)Send the token on every call as Authorization: Bearer $TOKEN. When it expires, the API answers 401: ask for a new one the same way.
2. Find your asset program
Everything you send goes to an asset program: the book of assets Fence keeps for you, with its own rules. List the programs your integration may use:
curl -s "$FENCE_API/v1/api/asset-programs" -H "Authorization: Bearer $TOKEN"{
"asset_programs": [
{
"id": "6f1c2b8e-4a77-4d0e-9a51-2f0f6c9d1e3a",
"originator_id": "0b9e5f42-1c3d-4f6a-8e7b-5d2a9c4e1f00",
"code": "ACME-LOANS",
"name": "Acme consumer loans",
"created_at": "2026-10-20T09:00:00Z"
}
],
"next_after": null
}Keep the program's id:
export PROGRAM_ID="6f1c2b8e-4a77-4d0e-9a51-2f0f6c9d1e3a"You only ever see your own programs. A program that is not yours answers 404, the same as one that does not exist.
3. Send an asset
Send a batch of records to the program. Each record is one asset: your own asset_id, its currency, and its details.
What details must hold is set by your program's rules: which fields, their types, and the checks Fence applies. Sign in to these docs and open My programs to see them for your program, with an example batch it accepts. The details below are only an example.
curl -s -X POST "$FENCE_API/v1/api/asset-programs/$PROGRAM_ID/submissions/assets" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"records": [
{
"asset_id": "L-123",
"currency": "EUR",
"details": {
"principal": 1200000,
"disbursement_date": "2026-10-01",
"annual_rate": "0.10"
}
}
]
}'Amounts are in minor units: 1200000 is 12,000.00 EUR.
The answer is 201 and describes the submission the call made:
{
"id": "a3d9e1c4-7b2f-4e8a-9c61-0d5f3b7a2e19",
"asset_program_id": "6f1c2b8e-4a77-4d0e-9a51-2f0f6c9d1e3a",
"kind": "assets",
"channel": "api_batch",
"status": "accepted",
"received_at": "2026-11-04T10:15:02.481920Z",
"settled_at": "2026-11-04T10:15:02.513377Z",
"record_count": 1,
"reasons": []
}accepted means the batch has the right shape and Fence has started on its records. If the shape is wrong, status is refused and reasons says why, each with the row and field it is about. A refused batch is still a 201: the call made a submission, and nothing in it was applied. Fix the batch and send it again.
Every call makes a new submission, even if you send the same body twice.
4. Read the batch back
The answer's Location header is the submission's address. Read it any time:
export SUBMISSION_ID="a3d9e1c4-7b2f-4e8a-9c61-0d5f3b7a2e19"
curl -s "$FENCE_API/v1/api/asset-programs/$PROGRAM_ID/submissions/$SUBMISSION_ID" \
-H "Authorization: Bearer $TOKEN"5. Check each record
An accepted batch can still hold records Fence refuses, for example a field your program does not allow. Fence decides each record on its own and shows the result on the submission's records:
curl -s "$FENCE_API/v1/api/asset-programs/$PROGRAM_ID/submissions/$SUBMISSION_ID/records" \
-H "Authorization: Bearer $TOKEN"{
"kind": "assets",
"total": 1,
"counts": { "received": 0, "new": 1, "changed": 0, "unchanged": 0, "refused": 0 },
"records": [
{
"position": 1,
"asset_id": "L-123",
"fence_asset_id": "5e2a7c90-3b1d-4f8e-a6c2-91d0e4b7f325",
"result": "new",
"reasons": [],
"warnings": []
}
],
"next_after": null
}Each record's result is one of:
received: Fence has not decided it yet. Read the records again shortly.new: Fence now holds this asset.changed: Fence held the asset, and this record changed it. Fence keeps the earlier version.unchanged: Fence held the asset exactly as sent.refused: Fence did not apply this record.reasonsnames the rule and the fields.
When the count of received is 0, every record is decided. Your asset is in Fence once its record is new.
What can go wrong
401: No token, or an expired one. Get a new token (step 1).403insufficient_scope: Your token lacks the scope this call needs. Ask for the scope in step 1. If your integration does not have it, ask Fence.404asset_program_not_found: The program does not exist, or is not yours. Check theidfrom step 2.413: The body is too large; nothing was stored. Split the batch.201withstatus: refused: The batch's shape is wrong. Readreasons, fix the batch, send it again.- A record
refused: The record breaks a rule of your program. Read the record'sreasons, fix it at the source, send it again.
Next
- Send the rest of your book: asset groups first, then assets, payment plans and payments.
- Read the API reference for every route and field.