Uploads API
Files are uploaded first and then referenced by URL: an escrow attachment_url, a chat attachment_url or dispute evidence_urls. Every upload goes through the same checks:
- The real type is read from the file's first bytes. It must match the declared type and the file extension, and be allowed for the upload type.
- Images are decoded and saved again, which removes EXIF data such as GPS location. Images above about 40 megapixels, or wider or taller than 12,000 pixels, are refused.
- PDFs are cleaned: scripts, automatic actions and attached files are removed, and only the cleaned copy is stored. A PDF locked with a password, a damaged PDF, or one with too many pages is refused.
- The file gets a name chosen by RekberPay and a SHA-256 hash, and is recorded against your account. Only you can attach it, and only once, to an escrow, a chat message or a dispute.
Uploaded files are not public. API answers return them as short-lived signed links (with exp, sig and usually u query parameters). A link is bound to the person it was issued to, and the file server checks on every fetch that they may still see the file. Use the link as returned and fetch the parent resource again for a fresh one.
Endpoints
| Method | Path | Endpoint | Auth |
|---|---|---|---|
GET | /v1/upload/config | Get the upload base URL | None |
POST | /v1/upload/:type | Upload a file | Key or session |
Get the upload base URL
The base URL that uploaded files are served from.
- Auth: None. Anyone can call it.
- Rate limit: 100 requests a minute per IP address.
- Idempotency: Not needed. Reading has no side effects.
Example request
curl https://api.rekberpay.com/v1/upload/configconst res = await fetch('https://api.rekberpay.com/v1/upload/config', {
});
const data = await res.json();import os
import requests
resp = requests.get(
"https://api.rekberpay.com/v1/upload/config",
timeout=30,
)
print(resp.status_code, resp.json())<?php
$ch = curl_init('https://api.rekberpay.com/v1/upload/config');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);Example response
200 OK
{
"success": true,
"data": {
"base_url": "https://api.rekberpay.com/uploads"
}
}Errors
| Status | Code | Cause |
|---|---|---|
429 | RATE_LIMITED | Over the rate limit. Wait for Retry-After seconds |
Upload a file
Upload one file as multipart/form-data in a field named file. The answer gives the URL to use in other calls.
| Type | Use | Max size | Allowed types |
|---|---|---|---|
escrow | Escrow attachments and dispute evidence | 10 MB | JPEG, PNG, WebP, PDF |
chat | Chat attachments | 5 MB | JPEG, PNG, WebP, PDF |
kyc | Identity documents, for the RekberPay app | 10 MB | JPEG, PNG, WebP |
- Auth: A personal API key (
Authorization: Bearer <key>) or a session cookie withX-CSRF-Tokenon writes. - Rate limit: 40 uploads per 10 minutes per user, on top of the shared limits. There is also a daily quota per user: 300 a day for each of
escrowandchat, 30 forkyc, and 400 in total. - Idempotency: Send an
Idempotency-Keyheader (UUID v4). A retry with the same key returns the first answer withIdempotent-Replay: true. - Language: Error messages are in Indonesian. Send
X-RekberPay-Lang: enfor English. Every upload error also carriesmessage_id(Indonesian) andmessage_en(English).
Path parameters
| Name | Type | Description |
|---|---|---|
type | string | escrow, chat or kyc |
Body (multipart/form-data)
| Name | Type | Required | Description |
|---|---|---|---|
file | file | Yes | The file. One per request |
Example request
curl -X POST https://api.rekberpay.com/v1/upload/escrow \
-H "Authorization: Bearer $REKBERPAY_API_KEY" \
-F "[email protected];type=image/png"import { readFile } from 'node:fs/promises';
const form = new FormData();
form.append('file', new Blob([await readFile('receipt.png')], { type: 'image/png' }), 'receipt.png');
const res = await fetch('https://api.rekberpay.com/v1/upload/escrow', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.REKBERPAY_API_KEY}`,
},
body: form,
});
const data = await res.json();import os
import requests
resp = requests.post(
"https://api.rekberpay.com/v1/upload/escrow",
headers={
"Authorization": f"Bearer {os.environ['REKBERPAY_API_KEY']}",
},
files={"file": ("receipt.png", open("receipt.png", "rb"), "image/png")},
timeout=30,
)
print(resp.status_code, resp.json())<?php
$ch = curl_init('https://api.rekberpay.com/v1/upload/escrow');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('REKBERPAY_API_KEY'),
],
CURLOPT_POSTFIELDS => ['file' => new CURLFile('receipt.png', 'image/png')],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);Example response
200 OK
{
"success": true,
"filename": "1791901398283_862e8cf475cf7693efed4855a91995d0_7e10e097a2087a40e3e8e3b659df3f65.png",
"upload_id": "1791901398283_862e8cf475cf7693efed4855a91995d0_7e10e097a2087a40e3e8e3b659df3f65.png",
"url": "https://api.rekberpay.com/uploads/escrow/1791901398283_862e8cf475cf7693efed4855a91995d0_7e10e097a2087a40e3e8e3b659df3f65.png",
"type": "escrow",
"mime": "image/png",
"size": 48213,
"sha256": "3f1d2c0b9e8a7f6e5d4c3b2a1908f7e6d5c4b3a29180f7e6d5c4b3a291807f6e"
}| Field | Type | Description |
|---|---|---|
url | string | The URL to send as attachment_url or in evidence_urls. Store it as returned |
filename, upload_id | string | The name RekberPay gave the file |
mime | string | The type found in the file, which can differ from what you declared only in name (a PNG stays a PNG) |
size | integer | Bytes stored, after images are saved again or PDFs cleaned |
sha256 | string | Hash of the stored bytes |
Errors
| Status | Code | Cause |
|---|---|---|
400 | INVALID_TYPE | type is not escrow, chat or kyc |
400 | NO_FILE | No file in a field named file, more than one file, or a broken multipart body |
400 | FILE_EMPTY | The file has no bytes |
413 | FILE_TOO_LARGE | The file is over the size limit for this type |
400 | TYPE_NOT_ALLOWED | The content is not JPEG, PNG, WebP or PDF, or that kind is not allowed for this type |
400 | TYPE_MISMATCH | The content does not match the declared type or the file extension |
400 | POLYGLOT | The file hides another format inside it |
400 | IMAGE_TOO_LARGE | The image has too many pixels |
400 | IMAGE_DECODE_FAILED | The image is damaged or not a real image |
400 | PDF_PASSWORD_PROTECTED | The PDF is locked with a password. Print it to a new PDF or upload a photo of the page |
400 | PDF_DAMAGED | The PDF cannot be opened |
400 | PDF_TOO_MANY_PAGES | The PDF has more pages than allowed |
400 | PDF_UNSUPPORTED_ENCODING, PDF_ACTIVE_CONTENT, PDF_EMBEDDED_FILE | The PDF could not be checked, or still had scripts or attached files after cleaning. Save it again as a PDF |
429 | QUOTA_EXCEEDED | The daily upload quota is used up |
500 | STORAGE_FAILED | The file was not stored. Try again |
401 | UNAUTHORIZED, SESSION_EXPIRED, INVALID_TOKEN | No session or API key, the session expired, or the key is wrong or revoked |
401 | WORKSPACE_KEY_ONLY | A workspace key (rpe_...) was sent. Those only work under /v1/enterprise |
403 | CSRF_FAILED | Session call without the matching X-CSRF-Token header |
403 | ACCOUNT_INACTIVE, SUSPENDED | The account is closed, being deleted or suspended |
429 | RATE_LIMITED | Over the rate limit. Wait for Retry-After seconds |
A PDF_PASSWORD_PROTECTED answer:
400 Bad Request
{
"success": false,
"error": {
"code": "PDF_PASSWORD_PROTECTED",
"message": "PDF ini dikunci dengan kata sandi. Buka file-nya, pilih Cetak lalu Simpan sebagai PDF, lalu unggah salinan itu. Bisa juga unggah foto atau tangkapan layar halamannya.",
"message_id": "PDF ini dikunci dengan kata sandi. Buka file-nya, pilih Cetak lalu Simpan sebagai PDF, lalu unggah salinan itu. Bisa juga unggah foto atau tangkapan layar halamannya.",
"message_en": "This PDF is locked with a password. Open it, choose Print, then Save as PDF, and upload that copy. You can also upload a photo or screenshot of the page."
}
}Attaching an upload
When you send an upload's URL to another call (an escrow attachment_url, a chat attachment_url or dispute evidence_urls), that call can refuse it:
| Status | Code | Cause |
|---|---|---|
400 | UPLOAD_INVALID_REF | The value is not a RekberPay upload URL |
400 | UPLOAD_NOT_FOUND | No such upload. Upload the file again |
400 | UPLOAD_NOT_OWNED | Someone else uploaded this file. Upload your own copy |
400 | UPLOAD_WRONG_PURPOSE | The upload type does not fit here, for example a kyc file in a chat |
409 | UPLOAD_ALREADY_BOUND | The file is already attached somewhere else |
409 | UPLOAD_QUOTA | This escrow or dispute already has as many attachments as allowed |