How a bank, broker or fintech app connects to Amtaar: send us investors, let them buy property shares from inside your product, and follow every order, payout and commission.
Audience: product and engineering teams at banks, brokers and fintech apps that partner with Amtaar · API version v1 (2026-10-18) · 18 Oct 2026
The complete endpoint reference (every field, every error) is at amtaar.app/developers/reference, generated from the OpenAPI 3.1 contract your developers can download and generate a client from (openapi.json). Partner access: hi@amtaar.dev.
Amtaar sells fractional ownership of rented commercial property in Egypt: an investor buys shares of a unit (one share = one square metre), receives a share of the rent every quarter, and owns a registered stake. The Partner API lets your product offer this to your customers in one of three ways. You can use one or several.
| Model | Your customer… | Money | What you build |
|---|---|---|---|
| A. Referral | taps a link in your app, installs Amtaar, invests there | paid to Amtaar in the Amtaar app | one API call to register the customer, then webhooks to show progress and commissions |
| B. Embedded, partner-settled | browses and buys Amtaar units inside your app | you debit your customer's account and transfer to Amtaar | catalogue, quote, order and settlement calls; status screens fed by webhooks |
| C. Embedded, card checkout | buys inside your app and pays by card on an Amtaar (Paymob) page | paid to Amtaar by card | catalogue and order calls; open the checkout page in a web view |
In every model you can also show your customers their holdings, rental income and wallet (with their consent and the matching permission), and track your commissions.
amtk_sandbox_…). The sandbox is a full copy of the platform with test data. It never moves real money and has extra endpoints that simulate what Amtaar staff and customers do.amtk_live_…), optionally limited to your server IP addresses.What your keys can do is bounded by capabilities that Amtaar switches on according to your agreement. GET /me always shows the current set.
| Capability | Allows | Typical for |
|---|---|---|
createCustomers |
open an Amtaar account for a person new to Amtaar | all models |
linkExistingCustomers |
link someone who already has an Amtaar account, after they confirm an SMS code | all models |
partnerKycReliance |
your own regulated KYC counts for orders placed through the API | B, C (needs a reliance agreement) |
partnerSettlementOrders |
place orders that you pay for and settle to Amtaar | B |
cardCheckoutOrders |
place orders paid on an Amtaar card page | C |
portfolioAccess |
holdings, rental payouts, contracts of linked customers | showing portfolios |
walletAccess |
Amtaar wallet balance of linked customers | showing portfolios |
Each API key also has scopes (for example orders:write), so you can give a reporting job a read-only key and your order service a write key.
Customer. A person you brought or linked, identified by cus_… (and by your own externalId if you send one). You only ever see customers linked to you.
Attribution. When you open a new Amtaar account through the API, that investor is attributed to you. Every purchase they make, in your app or in the Amtaar app, earns you the commission in your agreement. Existing Amtaar investors keep their existing attribution when you link them.
Consent. If the mobile number already belongs to an Amtaar investor, nothing about them is shared until they confirm. Amtaar texts them a 6-digit code, and you collect it in your app and send it to us (/consent, then /consent/verify).
KYC. Before anyone can invest they must be verified. Amtaar KYC happens in the Amtaar app: mobile number, e-mail, PIN, then national ID and face match. If your account has partnerKycReliance, you may instead attest your own KYC when you create the customer, and orders through the API will accept it. customer.kyc.missingSteps and customer.canInvest tell you where each customer stands.
Order. A purchase of N shares of one property at the current price, plus the management fee. Placing an order reserves the shares. An unpaid API order releases them at expiresAt (your hold time, 24 h by default; 60 minutes for card checkout).
Order statuses (stable; your screens can map them one to one):
| Status | Meaning | Next |
|---|---|---|
awaiting_payment |
shares reserved, waiting for the money | payment_under_review, paid, cancelled |
payment_under_review |
your settlement or the customer's transfer proof arrived; Amtaar Finance is checking it (normally within one working day) | paid, payment_rejected |
paid |
money confirmed. Shares are issued when the unit is fully funded | completed, cancellation_requested |
completed |
the customer owns the shares; the contract follows | — |
cancelled |
cancelled by you or the customer, or expired unpaid; shares released | — |
payment_rejected |
Finance could not match the payment | awaiting_payment (retry), cancelled |
cancellation_requested |
the customer asked to cancel a paid order; Amtaar reviews it (fees may apply) | cancelled, refunded, back to paid |
refunded |
money returned | — |
Environments. Sandbox and live are separate servers with separate keys and data. A sandbox key is refused by the live server, and the other way round.
Every request carries your key:
export AMTAAR=<sandbox base URL sent with your keys> # e.g. https://…/open/v1
export KEY=amtk_sandbox_xxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
curl -s $AMTAAR/me -H "Authorization: Bearer $KEY"1 — Show the catalogue and price a purchase.
curl -s "$AMTAAR/properties?status=available" -H "Authorization: Bearer $KEY"
curl -s -X POST $AMTAAR/quotes -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"property":"AMT-26-017","shares":10}'2 — Register your customer. Always send an Idempotency-Key. If the call times out, retry with the same key.
curl -s -X POST $AMTAAR/customers -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: 4f0c6a5e-2a3b-4f7e-9f1e-0c1d2e3f4a5b" \
-d '{"phone":"+201001234567","externalId":"your-user-88213","name":{"en":"Mona Ahmed","ar":"منى أحمد"},"language":"ar"}'The answer is a customer with status: "onboarding" (new account), "pending_consent" (existing investor, see section 5.1) or "active" (can invest).
3 — In sandbox, pretend the customer finished KYC.
curl -s -X POST $AMTAAR/sandbox/customers/cus_…/complete-kyc -H "Authorization: Bearer $KEY"4 — Place a partner-settled order with the settlement in the same call. amount must equal the quoted total.
curl -s -X POST $AMTAAR/orders -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: 8d2b1c0e-…" \
-d '{"customer":"cus_…","property":"AMT-26-017","shares":10,"externalReference":"your-order-55120",
"funding":{"method":"partner_settlement","settlement":{"reference":"TRX-99812","amount":102667.12}}}'5 — Move it along as Amtaar would (sandbox only), and watch your webhooks arrive:
curl -s -X POST $AMTAAR/sandbox/orders/ord_…/simulate -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d '{"to":"completed"}'
curl -s "$AMTAAR/customers/cus_…/holdings" -H "Authorization: Bearer $KEY"POST /customers with the customer's Egyptian mobile number (any common format; +2010…, 010…).customer.created). The customer later signs in to the Amtaar app with the same mobile number. There is nothing to sign up for again.pending_consent, phone shown masked. Call POST /customers/{id}/consent, ask the customer for the code we text them, then POST /customers/{id}/consent/verify (event customer.linked). Codes last 10 minutes; five wrong tries cancel a code.nationalId is used only to stop a second account for the same person. It is not stored.customer.kyc_verified arrives when they finish Amtaar KYC.Register the customer (5.1), then send them to customer.links.app (your tracked Amtaar download link). They invest in the Amtaar app, and you receive order.created, order.paid, order.completed for their orders (source: "app"), plus commission.created when your commission is booked.
GET /properties) and the price (POST /quotes).customer.canInvest).total.POST /orders with funding.method = "partner_settlement" and funding.settlement (your transfer reference and the exact amount). If you debit later, omit settlement and call POST /orders/{id}/settlement before expiresAt.paid (order.paid). If it cannot be matched it becomes payment_rejected; contact Amtaar with the reference.order.completed.If an order is rejected or cancelled after you debited your customer, refund your customer from your side. Amtaar returns the settled amount to you according to the agreement.
POST /orders with funding.method = "card_checkout". Open funding.checkoutUrl (Paymob) in a web view within 60 minutes. The order becomes paid when the card payment is confirmed, then completed as above.
With portfolioAccess: GET /customers/{id}/holdings (shares per property, invested amount, total rental income), /payouts (each quarterly rental payment) and /contracts (contract number and signing status). With walletAccess: /wallet. The payout.paid event tells you when new rent arrived.
GET /commissions lists every row of your ledger (signup fees, purchase commissions, bonuses, payouts to you, adjustments, reversals). GET /commissions/summary shows totals and the balance Amtaar owes you. Rows marked testMode were recorded while your account was in test mode and are not payable. A cancelled or refunded order reverses its commission.
Register an https endpoint (POST /webhook-endpoints, or ask Amtaar). Copy the signing secret from the answer; it is shown only once. Every event is POSTed as JSON:
{
"object": "event",
"id": "evt_6712c0ffee0123456789abcd",
"type": "order.paid",
"environment": "live",
"apiVersion": "2026-10-18",
"createdAt": "2026-10-18T09:41:07.000Z",
"data": { "order": { "object": "order", "id": "ord_…", "status": "paid", "…": "…" } }
}Verify every delivery. The header is Amtaar-Signature: t=<unix seconds>,v1=<hex>, where v1 = HMAC-SHA256(secret, t + "." + raw body).
// Node.js (Express): use the RAW body, not re-serialised JSON
const crypto = require('crypto');
app.post('/amtaar/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const [, t, v1] = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(req.get('Amtaar-Signature') || '') || [];
const expected = t && crypto.createHmac('sha256', process.env.AMTAAR_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest();
const fresh = t && Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!v1 || !fresh || !crypto.timingSafeEqual(Buffer.from(v1, 'hex'), expected)) return res.sendStatus(400);
const event = JSON.parse(req.body);
queue.add(event); // do the work asynchronously
res.sendStatus(200); // answer within 10 seconds
});# Python (Flask)
import hmac, hashlib, time, re
@app.post("/amtaar/webhooks")
def amtaar_webhook():
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", request.headers.get("Amtaar-Signature", ""))
raw = request.get_data()
if not m or abs(time.time() - int(m[1])) > 300:
abort(400)
expected = hmac.new(SECRET.encode(), m[1].encode() + b"." + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, m[2]):
abort(400)
enqueue(request.get_json())
return "", 200Delivery rules.
2xx answer within 10 seconds is a success. Otherwise we retry after 1 m, 5 m, 15 m, 1 h, 3 h, 6 h, 12 h and 24 h (9 attempts over about two days).PATCH /webhook-endpoints/{id} ("status":"enabled").Amtaar-Event-Id. When the order of events matters, re-read the object (GET /orders/{id}).GET /events?after=<last event id you processed> until hasMore is false. Events are kept 180 days.POST /webhook-endpoints/{id}/test sends a signed test event and shows you what your server answered.| Event | When |
|---|---|
customer.created |
you opened a new Amtaar account |
customer.linked |
an existing investor confirmed the link |
customer.kyc_verified |
a linked customer finished Amtaar KYC |
customer.unlinked |
a customer was unlinked |
order.created |
an order exists (placed by you, or by your customer in the app) |
order.payment_under_review |
payment evidence received; Finance is checking |
order.paid |
money confirmed |
order.completed |
shares issued to the customer |
order.cancelled |
cancelled or expired unpaid; shares released |
order.payment_rejected |
Finance rejected the payment |
order.cancellation_requested |
the customer asked to cancel a paid order |
order.refunded |
order refunded |
payout.paid |
rental income credited to the customer (needs portfolioAccess) |
commission.created |
a row was added to your commission ledger |
Every error has the same shape. Branch on code, show message only in logs, and quote requestId when you contact us.
{ "error": { "type": "permission_denied", "code": "customer_not_eligible",
"message": "The customer must finish verification before investing",
"details": { "missingSteps": ["identity_verification"] }, "requestId": "1f0c…" } }| HTTP | type |
Common codes |
What to do |
|---|---|---|---|
| 400 | invalid_request |
invalid_phone, invalid_shares, idempotency_key_required |
fix the request |
| 401 | authentication_error |
invalid_api_key, api_key_revoked, wrong_environment |
check the key and the base URL |
| 403 | permission_denied |
missing_scope, capability_not_enabled, customer_not_eligible, consent_required, ip_not_allowed |
permission or customer state — do not retry unchanged |
| 404 | not_found |
resource_not_found |
wrong id, or not linked to you |
| 409 | conflict |
external_id_in_use, already_settled, order_not_cancellable, state_conflict |
re-read the object; the state has moved on |
| 422 | unprocessable / idempotency_error |
amount_mismatch, order_limit_exceeded, daily_limit_exceeded, property_not_available, idempotency_key_reused |
valid request that cannot be done |
| 429 | rate_limited |
rate_limited |
wait Retry-After seconds |
| 500/503 | api_error / service_unavailable |
internal_error, checkout_unavailable, sms_unavailable |
retry with backoff, same Idempotency-Key |
POST /customers, POST /orders and POST /orders/{id}/settlement require an Idempotency-Key (a UUID per operation). A retry with the same key and body returns the original answer (Idempotent-Replayed: true) and never creates a second customer or order. Keys are remembered for 24 hours.RateLimit-* headers.GET /me → limits).DELETE /customers/{id} ends your access to a customer. Their Amtaar account and investments are unaffected.| You want to test | How |
|---|---|
| A customer finishing KYC | POST /sandbox/customers/{id}/complete-kyc |
| Finance accepting a settlement | POST /sandbox/orders/{id}/simulate {"to":"paid"} |
| Shares being issued | {"to":"completed"} |
| A rejected payment | {"to":"payment_rejected"} |
| Consent of an existing investor | POST /customers/{id}/consent returns sandboxCode instead of texting |
| Your webhook handler | POST /webhook-endpoints/{id}/test; sandbox also accepts http:// URLs (e.g. a tunnel to your laptop) |
| Expiry | place an order and leave it unpaid past expiresAt |
| Area | Endpoints |
|---|---|
| Account | GET /me |
| Properties | GET /properties, GET /properties/{id or code}, POST /quotes |
| Customers | POST /customers, GET /customers, GET/PATCH/DELETE /customers/{id}, POST /customers/{id}/consent, POST /customers/{id}/consent/verify |
| Portfolio | GET /customers/{id}/holdings, /payouts, /contracts, /wallet |
| Orders | POST /orders, GET /orders, GET /orders/{id}, POST /orders/{id}/settlement, POST /orders/{id}/cancel |
| Commissions | GET /commissions, GET /commissions/summary |
| Events | GET /events, GET /events/{id} |
| Webhooks | GET/POST /webhook-endpoints, GET/PATCH/DELETE /webhook-endpoints/{id}, POST …/rotate-secret, POST …/test, GET /webhook-deliveries, POST /webhook-deliveries/{id}/retry |
| Sandbox | POST /sandbox/customers/{id}/complete-kyc, POST /sandbox/orders/{id}/simulate |
Does my customer need to download the Amtaar app? In model A, yes. In models B and C they can invest entirely inside your app, if their KYC is done (Amtaar KYC in the app, or your KYC under a reliance agreement). They can always open the Amtaar app later with the same mobile number to see their investment and contracts.
Why is an order paid but not completed? Shares are issued when the whole unit is funded. Until then the money is held against the order. If a unit does not reach its funding, Amtaar contacts investors under its funding terms.
Can I cancel a paid order? Not through the API. The customer requests a cancellation in the Amtaar app, and Amtaar reviews it (order.cancellation_requested). Cancellation fees may apply.
Do I see orders my customers place in the Amtaar app? Yes, for customers linked to you. They have source: "app" and funding.method: "in_app".
What about international mobile numbers? v1 accepts Egyptian mobile numbers. Customers abroad with an Egyptian number are supported; other numbers will come later.
Can I sell shares on behalf of a customer? Not yet. The resale market is planned and will be added to v1 as new endpoints when it launches.
Contact: Amtaar partnerships — hi@amtaar.dev · amtaar.app/developers. Quote the requestId of any failing call.