1. Overview
Artham is a marketplace platform that connects fund managers (Investment Advisors registered with SEBI) with clients through distribution partners. As part of this integration, Artham's servers call your (the broker's) APIs — to fetch client information, and to push executed trades to a receiver you host. One endpoint runs the other way round: Artham hosts it, and you call it once a day to pull your clients' holdings.
How It Works
- Artham calls your APIs — you build the endpoints described in this document.
- You verify our identity — using the API key + HMAC signature we send with every request.
- You return JSON responses — in the standard format described below.
- You call one Artham API — API 3, the daily holdings pull. Artham hosts it and you are the caller, so the roles above are reversed: you sign the request, we verify it and return the JSON.
API Summary
| # | Method | Endpoint | Purpose | |
|---|---|---|---|---|
| 1 | GET | /api/v1/clients/{ppCode}/{clientCode} | Fetch client KYC and account details | Required |
| 2 | POST | {your-trades-receiver-URL} | Receive the daily batch of executed trades (fills) — Artham pushes, you host | Required |
| 3 | GET | /api/v1/broker/holdings | Pull all your clients' equity holdings — Artham hosts, you call | Required |
Base URLs
Two of the three endpoints live on your servers; API 3 lives on ours.
Hosted by you
You provide these to Artham. The API 1 path is appended to your base URL; the API 2 receiver is a full URL of your choosing.
Production: https://api.yourbroker.com
Sandbox: https://sandbox-api.yourbroker.com
Hosted by Artham
The API 3 path is appended to the base URL for the environment you are calling.
Production: https://api.artham.co
Sandbox: https://distributor-api.artham.co
2. Authentication
The whole integration uses one authentication scheme, in both directions. Every request carries two headers — Artham sends them on API 1 and API 2; you send them on API 3:
| Header | Description | Example |
|---|---|---|
X-Api-Key |
Static API key identifying the caller. Artham sends it on API 1 / API 2; you send the same key back to us on API 3, where it identifies your brokerage. | artham_live_k8Fj2mNp... |
X-Signature |
HMAC-SHA256 signature of the request, computed using a shared secret. Proves the request hasn't been tampered with. | a1b2c3d4e5f6... (64 hex chars) |
Credential Setup
- Artham generates an API Key (public identifier) and a Shared Secret (private, used for HMAC) for each broker.
- Artham will share both credentials with you over email.
- You store the API Key and Shared Secret on your server. Use them to verify the requests Artham sends you (API 1, API 2), and to sign the requests you send Artham (API 3).
Signature Computation
The signature is computed the same way by whichever side is calling:
message = "{HTTP_METHOD}\n{path}\n{body}"
signature = HMAC-SHA256(key: SHARED_SECRET, message: message)
→ output as lowercase hex string
| Component | Description | Example |
|---|---|---|
HTTP_METHOD | Uppercase HTTP method | GET |
path | Request path (no query string, no host) | /api/v1/clients/PP001/AB1234 |
body | Raw JSON request body. Empty string "" for GET requests. | "" (GET has no body) |
"GET\n/api/v1/clients/PP001/AB1234\n"
Verifying — Artham → You
- Check API key — look up the
X-Api-Keyheader value. Reject with401if it doesn't match the key Artham shared with you. - Recompute signature — using the Shared Secret, compute the HMAC-SHA256 of the same message format shown above.
- Compare — use a constant-time comparison. Reject with
401if signatures don't match.
Signing — You → Artham
- Build the message —
"{METHOD}\n{path}\n{body}", exactly as above. API 3 is aGETwith no body, so the message is"GET\n/api/v1/broker/holdings\n". - Compute the HMAC — HMAC-SHA256 of that message with your Shared Secret, as a lowercase hex string.
- Send both headers —
X-Api-Key(your key, unchanged) andX-Signature(the hex digest). Artham runs the same three verification steps in reverse and answers401if either fails.
/api/v1/broker/holdings, not the full https://… URL. This is the single most common cause of a 401 on a first integration.
See Section 7 for complete code in Python, Java, and C#. The computation is identical whether you are verifying or signing.
3. Error Handling
Response Envelope
All responses must follow this structure. The APIs you build must use it, and Artham's own API 3 response uses it too — so a single parser handles every response in this document.
Success
{
"success": true,
"data": { ... },
"error": null
}
Error
{
"success": false,
"data": null,
"error": {
"code": "CLIENT_NOT_FOUND",
"message": "No client found with code AB9999."
}
}
HTTP Status Codes
| Status | When to Use |
|---|---|
200 | Success |
400 | Bad request (invalid params, missing fields) |
401 | Invalid API key or signature |
404 | Client not found |
422 | Business rule violation (insufficient limit, etc.) |
500 | Internal server error |
Error Codes
Use these standard error codes in the error.code field:
| Code | Description |
|---|---|
CLIENT_NOT_FOUND | The given clientCode does not exist in your system |
CLIENT_INACTIVE | Client account is deactivated or suspended |
INSUFFICIENT_LIMIT | Client does not have enough available limit/balance |
INVALID_REQUEST | Missing or malformed request parameters |
INTERNAL_ERROR | Unexpected server error on your side |
4. API 1 — Get Client Details Required
This is the primary API. Artham calls this to fetch a client's KYC information, personal details, and bank account details when a client is being onboarded onto the advisory platform.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
ppCode | string | The portfolio provider code assigned to your brokerage |
clientCode | string | The client's trading code at your brokerage |
Headers
Authentication uses the X-Api-Key + X-Signature headers described in Section 2. For this GET request, calculate the HMAC-SHA256 signature using the complete request path and an empty body.
| Header | Required | Description | Example |
|---|---|---|---|
X-Api-Key | Yes | Static API key identifying Artham. | artham_live_k8Fj2mNp... |
X-Signature | Yes | Lowercase hexadecimal HMAC-SHA256 signature for "GET Client Details API". | a1b2c3d4... (64 hex chars) |
Response — 200 OK
{
"success": true,
"data": {
"clientCode": "AB1234",
"rmCode": "RM001",
"name": "Rajesh Kumar",
"pan": "ABCDE1234F",
"dateOfBirth": "1990-05-15",
"address1": "123 MG Road",
"address2": "Andheri West",
"address3": "",
"city": "Mumbai",
"state": "Maharashtra",
"pinCode": "400058",
"emailAddress": "rajesh@example.com",
"mobileNumber": "9876543210",
"accountNo": "1234567890",
"ifscCode": "HDFC0001234",
"accountType": "S",
"clientTypeCode": 1,
"isNRI": false,
"isPoliticallyExposedPerson": false
},
"error": null
}
Response Fields
| Field | Type | Required | Description |
|---|---|---|---|
clientCode | string | Yes | Client's unique trading code at your brokerage |
rmCode | string | Yes | Relationship Manager code assigned to this client |
name | string | Yes | Full name as registered in your system |
pan | string | Yes | PAN number (10 characters, e.g. ABCDE1234F) |
dateOfBirth | string | Yes | Date of birth in YYYY-MM-DD format |
address1 | string | No | Address line 1 |
address2 | string | No | Address line 2 |
address3 | string | No | Address line 3 |
city | string | No | City |
state | string | No | State |
pinCode | string | No | PIN code (6 digits) |
emailAddress | string | Yes | Registered email address |
mobileNumber | string | Yes | Registered mobile number (10 digits) |
accountNo | string | Yes | Client's primary bank account number |
ifscCode | string | Yes | IFSC code of the bank branch (11 characters) |
accountType | string | Yes | Bank account type: "S" (Savings), "C" (Current), or "O" (Other) |
clientTypeCode | integer | Yes | Client constitution code. Return one of the supported values listed below. |
isNRI | boolean | Yes | true if the client is a Non-Resident Indian |
isPoliticallyExposedPerson | boolean | Yes | true if the client is a Politically Exposed Person (PEP) |
pan, name, dateOfBirth, and bank account details — are accurate and match your KYC records.
Field Details
accountType values:
| Value | Description |
|---|---|
"S" | Savings account |
"C" | Current account |
"O" | Other |
clientTypeCode values:
| Value | Description |
|---|---|
1 | Individual |
2 | Mutual Fund |
3 | Body Corporate |
4 | Non-Tax Paying Entity |
5 | Others |
6 | Hindu Undivided Family |
7 | Oversees Corporate Body |
8 | Partnership Firm |
9 | Merchant Banker |
10 | Foreign Institutional Investor |
11 | Indian Financial Instituion |
12 | Banks |
13 | Company |
14 | Trust |
15 | Financial Institution |
16 | NBFC |
17 | Society |
18 | NRI |
19 | Statutory Bodies |
20 | Insurance Companies |
21 | Proprietor |
22 | National Pension Scheme |
23 | Depository Receipts |
24 | Foreign Direct Investments |
25 | Foreign Venture Capital Funds |
26 | Non Govt. Organisation |
27 | PMS IND |
28 | PMS NON-IND |
29 | QFI IND |
30 | QFI Others |
31 | LIMITED LIABILITY PARTNERSHIPS |
32 | Non-Banking Financial Company (NBFC) |
33 | Alternate Investment Fund |
34 | Foreign National |
35 | Domestic Venture Capital Fund |
36 | FPI 1 (INS) |
37 | FPI 2 (INS) |
38 | FPI 3 (INS) |
39 | FPI 1 (I) |
40 | FPI 2 (I) |
41 | FPI 3 (I) |
42 | FPI 1 (NI) |
43 | FPI 2 (NI) |
44 | FPI 3 (NI) |
71 | NRO |
If your system uses different codes, please share your mapping and we will align.
Error Responses
| HTTP | Error Code | When |
|---|---|---|
401 | — | Invalid API key or signature |
404 | CLIENT_NOT_FOUND | No client exists with this clientCode |
422 | CLIENT_INACTIVE | Client account is deactivated or suspended |
5. API 2 — Executed Trades Push Required
After a dealer uploads the end-of-day trade file, Artham pushes that broker's executed trades (fills) to a receiver endpoint that you host. This is an event-driven push — Artham is the caller. There is no date parameter and no polling; each record carries its own timestamp.
Endpoint & Method
| Item | Value |
|---|---|
| Method | POST |
| URL | You host it — send Artham your Sandbox and Production receiver URLs |
| TLS | TLS 1.2+ over HTTPS |
| IP allowlisting | No restriction on Artham's side. As the caller, Artham can share its egress IPs for you to allowlist. |
Headers
Authentication uses the same X-Api-Key + X-Signature headers described in Section 2 — the signature is HMAC-SHA256 over "{METHOD}\n{path}\n{body}", where body is the raw JSON payload of this POST.
| Header | Description | Example |
|---|---|---|
X-Api-Key | Static API key identifying Artham (see Section 2). | artham_live_k8Fj2mNp... |
X-Signature | HMAC-SHA256 of the request, computed over the raw body (see Section 2). | a1b2c3d4... (64 hex) |
Content-Type | Always application/json. | application/json |
Request Body
The whole batch is a single JSON object with one array property, trades. There is no pagination and no date / branch / segment filter.
{
"trades": [
{
"clientCode": "AB1234",
"exchange": "NSE",
"segment": "CM",
"scripName": "TCS",
"transactionType": "BUY",
"isin": "INE467B01029",
"quantity": "10",
"price": "3450.50",
"productType": "Delivery",
"orderType": "Limit",
"exchangeOrderNumber": "1100000012345678",
"timestamp": "27 FEB 2026 14:36:49"
}
]
}
Record Fields 12 keys · camelCase
Each element of trades has exactly these 12 keys — no extra fields.
| # | Key | Type | Description & format |
|---|---|---|---|
| 1 | clientCode | string | Client / UCC code |
| 2 | exchange | string | "NSE" or "BSE" |
| 3 | segment | string | "CM" (Cash Market) or "FO" (Futures & Options) |
| 4 | scripName | string | OMS symbol |
| 5 | transactionType | string | Trade side — "BUY" or "SELL" |
| 6 | isin | string | 12-char ISIN, e.g. "INE467B01029" |
| 7 | quantity | string | Positive integer as a string, e.g. "10" |
| 8 | price | string | Trade Price |
| 9 | productType | string | One of "Intraday", "Delivery", "MTF", "Normal Carry Forward" |
| 10 | orderType | string | One of "Market" or "Limit" |
| 11 | exchangeOrderNumber | string | Exchange order number |
| 12 | timestamp | string | IST string, e.g. "27 FEB 2026 14:36:49" |
quantity, price) are sent as JSON strings, not numbers.
Response
Artham treats any 2xx as success. On success, respond with 200 OK and the following JSON envelope (Content-Type: application/json):
{
"status": true,
"code": 200,
"message": "Trades received successfully"
}
| Field | Type | Description |
|---|---|---|
status | boolean | true if the batch was accepted, false if it was rejected |
code | integer | Application status code — 200 on success |
message | string | Human-readable result message |
Retries & Resilience Artham is the caller
How Artham reacts to the HTTP status your endpoint returns:
| Status your endpoint returns | What it means | Artham's action |
|---|---|---|
2xx — e.g. 200 OK | Success — batch received and accepted | Done. Never re-sent. |
408 Request Timeout | You didn't finish reading / processing the request in time | Retry |
429 Too Many Requests | You're rate-limiting us | Retry (with backoff) |
5xx — 500, 502, 503, 504 | Server error on your side — usually transient | Retry |
| Network error / timeout | No HTTP response at all (connection drop, DNS / TLS failure, read timeout) | Retry |
Any other 4xx — 400, 401, 403, 404, 422 | Client error — the request is rejected as-is; retrying won't help | Stop. No retry. Please share sample error bodies. |
| Behaviour | Detail |
|---|---|
| Re-delivery | Retries re-send the identical batch body. Artham never re-sends a batch it has already accepted — but processing the same batch twice should be safe on your side. |
| Retry policy | Up to 5 attempts, exponential backoff (base 60 s). A reconciler also sweeps stale rows every 5 min. |
| Timeout | 15 s per attempt (configurable); your 30 s is fine. |
| Rate limits | None on Artham's side (low, event-driven volume). Tell us yours and we'll respect them. |
Volume & Scheduling
- One row per fill; a batch is that broker's fills for the day.
- Event-driven — pushed right after the dealer uploads the EOD trade file.
6. API 3 — Client Holdings Snapshot Required
A once-a-day snapshot of the equity holdings and cash balances of every client mapped to your brokerage on Artham, returned in a single response. It exists so you can store the advisory-side position of your clients in your own database and reconcile it against your books.
X-Api-Key + X-Signature scheme described in Section 2, with the same credential pair — you simply compute the signature instead of verifying it. There is no separate login, no token and no session to manage.
Endpoint & Method
| Item | Value |
|---|---|
| Method | GET |
| Path | /api/v1/broker/holdings |
| Base URL | Artham-hosted — see Section 1 |
| Authentication | X-Api-Key + X-Signature, signed by you — see Section 2 |
| Query parameters | None. There is no date, client or exchange filter — the response is always the current snapshot for all of your clients. |
| Pagination | None. The full set is returned in one response. |
BrokerId in the request — the key resolves to your brokerage on our side, and can only ever return your own clients' positions.
Headers
| Header | Required | Description |
|---|---|---|
X-Api-Key | Yes | The API key Artham issued you — the same one you use to recognise our calls on API 1 and API 2. |
X-Signature | Yes | Lowercase hex HMAC-SHA256 over "GET\n/api/v1/broker/holdings\n". 64 characters. |
No Content-Type is needed — this is a GET with no body.
Signing This Request
The body is empty, so the message ends with a trailing newline:
message = "GET\n/api/v1/broker/holdings\n"
signature = HMAC-SHA256(key: SHARED_SECRET, message: message)
→ lowercase hex
Use this fixed pair to check your implementation before your credentials arrive — the same message with this demo secret must produce exactly this digest:
secret = "artham_demo_secret_9f2b7c41e8d3"
message = "GET\n/api/v1/broker/holdings\n"
signature = e1ed253a76977a84da1ff548a51db899426044fe2c0c62c6adc911b73204584a
The live call, against sandbox:
curl -X GET "https://distributor-api.artham.co/api/v1/broker/holdings" \
-H "X-Api-Key: <your API key>" \
-H "X-Signature: <64-char lowercase hex>"
Response — 200 OK
{
"success": true,
"error": null,
"data": {
"asOf": "2026-09-01",
"clientCount": 128,
"holdingCount": 1043,
"cashCount": 121,
"holdings": [
{ "clientCode": "AR001234", "isin": "INE002A01018", "symbol": "RELIANCE", "exchange": "NSE", "quantity": "25.0000" },
{ "clientCode": "AR001234", "isin": "INE467B01029", "symbol": "TCS", "exchange": "NSE", "quantity": "8.0000" },
{ "clientCode": "AR005678", "isin": "INE009A01021", "symbol": "INFY", "exchange": "NSE", "quantity": "40.0000" },
{ "clientCode": "AR005678", "isin": "INE002A01018", "symbol": "500325", "exchange": "BSE", "quantity": "10.0000" }
],
"cash": [
{ "clientCode": "AR001234", "amount": "10018.67" },
{ "clientCode": "AR005678", "amount": "205.02" }
]
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | true on success — the standard envelope from Section 3 |
error | object | null on success; on failure carries code and message |
data.asOf | string | Date the snapshot was taken, YYYY-MM-DD in IST — the quantities are as at the moment of the call, not a frozen end-of-day position |
data.clientCount | integer | Number of distinct clientCode values across both arrays — a client can appear in one without the other |
data.holdingCount | integer | Number of rows in holdings. Use it to confirm nothing was truncated in transit. |
data.cashCount | integer | Number of rows in cash. Same purpose. |
data.holdings | array | Flat array of holding rows — see below. Empty array if you have no clients with positions. |
data.cash | array | One row per client with a non-zero cash balance — see below. Empty array if no client holds cash. |
Holding Record Fields 5 keys · camelCase
The array is flat — one row per client per scrip, not nested per client. Each element has exactly these 5 keys.
| # | Key | Type | Description & format |
|---|---|---|---|
| 1 | clientCode | string | Client / UCC code — the same code used in API 1 and API 2 |
| 2 | isin | string or null | 12-character ISIN, e.g. "INE002A01018". Exchange-agnostic — the same ISIN appears for a scrip whether it is reported on NSE or BSE. null when Artham holds no ISIN for that position (see the note below). |
| 3 | symbol | string | Trading symbol on the exchange named in exchange. NSE trading symbol (e.g. "RELIANCE") when exchange is "NSE"; BSE scrip code (e.g. "500325") when it is "BSE". |
| 4 | exchange | string | "NSE" or "BSE" |
| 5 | quantity | string | Net quantity held, as a decimal string with 4 decimal places — e.g. "25.0000". Always greater than zero — see the scope rules below. Parse it as a decimal, not an integer (see the note below). |
quantity as a decimal, not an integer. It is sent as a JSON string with 4 decimal places — the same convention API 2 uses for quantity and price. Whole numbers are the norm, but a reverse split on an odd holding leaves a genuine fraction ("12.5000"), so an integer parser will fail the first time one occurs.
Cash Record Fields 2 keys · camelCase
One row per client, not per portfolio — the same aggregation the holdings array uses.
| # | Key | Type | Description & format |
|---|---|---|---|
| 1 | clientCode | string | Client / UCC code — joins to the same code in holdings |
| 2 | amount | string | Cash balance in rupees, as a decimal string with 2 decimal places — e.g. "10018.67". Can be negative (see below). |
cash and not in holdings — money subscribed but not yet deployed — or in holdings and not in cash, when their balance is exactly zero. Do not assume a client in one array is present in the other.
Scope Rules
These rules decide what appears in holdings. Please mirror them on your side so reconciliation differences are real differences.
| Rule | Detail |
|---|---|
| Equity only | Mutual fund and bond positions are excluded — they have no exchange trading symbol, so symbol / exchange would be meaningless. |
| Positive quantities only | Rows with quantity of zero or less are omitted. A position the client has fully exited simply disappears from the response. |
| Aggregated per client | A client who holds the same scrip across more than one advisory portfolio appears as one row, with quantities summed. Cash is summed across their portfolios the same way. There is no portfolio dimension in this response. |
| Non-zero cash only | A client whose balance is exactly zero is omitted from cash. Treat absence from that array as a zero balance, not as missing data. |
| Exchange is derived | Artham stores the NSE symbol and the BSE scrip code separately. A position with an NSE symbol is reported as NSE; otherwise it is reported as BSE with the numeric scrip code. This is the same resolution Artham uses when placing the order, so the identifier you receive is the one the position was traded on. |
| Your clients only | Scoped to clients whose broker mapping on Artham is you. Clients of other brokers are never returned. |
Timing & Scheduling
| Behaviour | Detail |
|---|---|
| Frequency | Once per day. The endpoint is read-only, so re-calling is safe — but it is a live read, not a fixed daily file: a second call later the same day may differ as that day's trades settle. |
| Weekends / holidays | Returns the last completed business day's positions. Safe to call daily. |
| Retries | On 5xx or a network error, retry with backoff. A 401 will not fix itself on a retry — the key or the signed message is wrong, so check those rather than looping. |
Error Responses
Errors use the standard envelope from Section 3, with data set to null.
{
"success": false,
"data": null,
"error": {
"code": "AUTH_TOKEN_INVALID",
"message": "Invalid API key or signature."
}
}
| HTTP | error.code | When | What to do |
|---|---|---|---|
401 | AUTH_REQUIRED | X-Api-Key or X-Signature missing | Send both headers |
401 | AUTH_TOKEN_INVALID | Unknown API key, or the signature does not match the message we recomputed | Check you signed the path only and included the trailing newline for the empty body |
500 | INTERNAL_ERROR | Unexpected error on Artham's side | Retry with backoff |
error.message text is for humans reading logs and may change; the status code and error.code are the contract.
7. Signature — Code Samples
Complete examples for both halves of the integration, in each supported stack. The first group verifies the signature on requests Artham sends you (API 1, API 2). The second group signs and sends your daily API 3 call.
"{METHOD}\n{path}\n{body}" message and run the identical HMAC — the only difference is whether you compare the digest or send it. If you already implemented verification for API 1, calling API 3 reuses that function unchanged.
Python — Verifying (API 1 & 2)
import hmac
import hashlib
def verify_signature(shared_secret, method, path, body, received_signature):
"""
Verify the HMAC-SHA256 signature sent by Artham.
Args:
shared_secret: The secret shared by Artham for your broker
method: HTTP method, e.g. "GET"
path: Request path, e.g. "/api/v1/clients/PP001/AB1234"
body: Raw request body as string ("" for GET requests)
received_signature: The X-Signature header value
Returns:
True if signature is valid
"""
message = f"{method}\n{path}\n{body}"
expected = hmac.new(
shared_secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected, received_signature)
# ─── Example: Flask middleware ───────────────────────────────
from flask import request, abort
# The credentials Artham shared with you
ARTHAM_API_KEY = "artham_live_k8Fj2mNp..."
ARTHAM_SHARED_SECRET = "your_shared_secret_here"
@app.before_request
def check_artham_signature():
api_key = request.headers.get('X-Api-Key')
signature = request.headers.get('X-Signature')
if not api_key or not signature:
abort(401, 'Missing authentication headers')
if api_key != ARTHAM_API_KEY:
abort(401, 'Unknown API key')
body = request.get_data(as_text=True) or ""
if not verify_signature(
ARTHAM_SHARED_SECRET,
request.method, request.path, body, signature
):
abort(401, 'Invalid signature')
Java — Verifying (API 1 & 2)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class ArthamSignatureVerifier {
/**
* Verify the HMAC-SHA256 signature sent by Artham.
*
* @param sharedSecret The secret shared by Artham for your broker
* @param method HTTP method, e.g. "GET"
* @param path Request path, e.g. "/api/v1/clients/PP001/AB1234"
* @param body Raw request body ("" for GET requests)
* @param receivedSig The X-Signature header value
*/
public static boolean verifySignature(
String sharedSecret,
String method,
String path,
String body,
String receivedSig
) throws Exception {
String message = method + "\n" + path + "\n" + (body != null ? body : "");
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(
sharedSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
);
mac.init(keySpec);
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
String expected = bytesToHex(hash);
// Constant-time comparison
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
receivedSig.getBytes(StandardCharsets.UTF_8)
);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
// ─── Example: Spring Boot filter ────────────────────────────
// @Component
// public class ArthamAuthFilter extends OncePerRequestFilter {
// @Value("${artham.api-key}") private String arthamApiKey;
// @Value("${artham.shared-secret}") private String arthamSecret;
//
// @Override
// protected void doFilterInternal(HttpServletRequest req,
// HttpServletResponse res, FilterChain chain) throws ... {
// String apiKey = req.getHeader("X-Api-Key");
// String signature = req.getHeader("X-Signature");
//
// if (!arthamApiKey.equals(apiKey)) {
// res.sendError(401, "Unknown API key"); return;
// }
//
// String body = new String(req.getInputStream().readAllBytes());
// if (!ArthamSignatureVerifier.verifySignature(
// arthamSecret, req.getMethod(),
// req.getRequestURI(), body, signature)) {
// res.sendError(401, "Invalid signature"); return;
// }
// chain.doFilter(req, res);
// }
// }
C# (.NET) — Verifying (API 1 & 2)
using System.Security.Cryptography;
using System.Text;
public static class ArthamSignatureVerifier
{
/// <summary>
/// Verify the HMAC-SHA256 signature sent by Artham.
/// </summary>
/// <param name="sharedSecret">The secret shared by Artham</param>
/// <param name="method">HTTP method, e.g. "GET"</param>
/// <param name="path">Request path, e.g. "/api/v1/clients/PP001/AB1234"</param>
/// <param name="body">Raw request body ("" for GET)</param>
/// <param name="receivedSignature">X-Signature header value</param>
public static bool VerifySignature(
string sharedSecret,
string method,
string path,
string body,
string receivedSignature)
{
string message = $"{method}\n{path}\n{body ?? ""}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(sharedSecret));
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
string expected = BitConverter.ToString(hash).Replace("-", "").ToLower();
// Constant-time comparison
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(receivedSignature)
);
}
}
// ─── Example: ASP.NET Core middleware ───────────────────────
// public class ArthamAuthMiddleware {
// private readonly RequestDelegate _next;
// private readonly string _apiKey; // from appsettings.json
// private readonly string _secret; // from appsettings.json
//
// public async Task InvokeAsync(HttpContext context) {
// var apiKey = context.Request.Headers["X-Api-Key"].FirstOrDefault();
// var signature = context.Request.Headers["X-Signature"].FirstOrDefault();
//
// if (apiKey != _apiKey) {
// context.Response.StatusCode = 401; return;
// }
//
// context.Request.EnableBuffering();
// using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
// var body = await reader.ReadToEndAsync();
// context.Request.Body.Position = 0;
//
// if (!ArthamSignatureVerifier.VerifySignature(
// _secret, context.Request.Method,
// context.Request.Path, body, signature)) {
// context.Response.StatusCode = 401; return;
// }
// await _next(context);
// }
// }
Python — Calling API 3
import hmac
import hashlib
import requests
ARTHAM_BASE = "https://distributor-api.artham.co" # Sandbox
API_KEY = "artham_live_k8Fj2mNp..."
SHARED_SECRET = "your_shared_secret_here"
PATH = "/api/v1/broker/holdings"
def sign(shared_secret, method, path, body=""):
"""HMAC-SHA256 of "{METHOD}\n{path}\n{body}" as lowercase hex."""
message = f"{method}\n{path}\n{body}"
return hmac.new(
shared_secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
def fetch_holdings():
response = requests.get(
ARTHAM_BASE + PATH,
headers={
"X-Api-Key": API_KEY,
# GET has no body, so the signed message is "GET\n{PATH}\n"
"X-Signature": sign(SHARED_SECRET, "GET", PATH),
},
timeout=60,
)
response.raise_for_status()
payload = response.json()
if not payload["success"]:
raise RuntimeError(payload["error"]["message"])
data = payload["data"]
# holdingCount is there so you can prove nothing was lost in transit
if len(data["holdings"]) != data["holdingCount"]:
raise RuntimeError("truncated response — do not store this snapshot")
return data["asOf"], data["holdings"]
# ─── Daily job, after 07:00 IST ──────────────────────────────
as_of, rows = fetch_holdings()
# REPLACE the stored set — a closed position is expressed by its absence,
# so merging would keep exited positions alive forever.
replace_stored_holdings(as_of, rows)
Java — Calling API 3
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public class ArthamHoldingsClient {
private static final String BASE = "https://distributor-api.artham.co"; // Sandbox
private static final String PATH = "/api/v1/broker/holdings";
private static final String KEY = "artham_live_k8Fj2mNp...";
private static final String SECRET = "your_shared_secret_here";
/**
* HMAC-SHA256 of "{METHOD}\n{path}\n{body}" as lowercase hex.
* Identical to the verification helper — only the use differs.
*/
static String sign(String secret, String method, String path, String body)
throws Exception {
String message = method + "\n" + path + "\n" + body;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(raw.length * 2);
for (byte b : raw) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
/** Returns the raw JSON body — parse it with your JSON library. */
public static String fetchHoldings() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE + PATH))
.header("X-Api-Key", KEY)
// GET has no body, so the body component is the empty string
.header("X-Signature", sign(SECRET, "GET", PATH, ""))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
// 401 will not fix itself on a retry — check the key and the
// signed path. Retry with backoff only on 5xx.
throw new IllegalStateException(
"Artham returned " + response.statusCode() + ": " + response.body());
}
return response.body();
}
}
C# (.NET) — Calling API 3
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
public class ArthamHoldingsClient
{
private const string BaseUrl = "https://distributor-api.artham.co"; // Sandbox
private const string Path = "/api/v1/broker/holdings";
private const string Key = "artham_live_k8Fj2mNp...";
private const string Secret = "your_shared_secret_here";
private static readonly HttpClient Http = new HttpClient();
/// <summary>
/// HMAC-SHA256 of "{METHOD}\n{path}\n{body}" as lowercase hex.
/// </summary>
public static string Sign(string secret, string method, string path, string body = "")
{
var message = $"{method}\n{path}\n{body}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
return Convert.ToHexString(hash).ToLowerInvariant();
}
/// <summary>Returns the raw JSON body — parse it with System.Text.Json.</summary>
public static async Task<string> FetchHoldingsAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, BaseUrl + Path);
request.Headers.Add("X-Api-Key", Key);
// GET has no body, so the body component is the empty string
request.Headers.Add("X-Signature", Sign(Secret, "GET", Path));
var response = await Http.SendAsync(request);
var payload = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
// 401 means the key or the signed path is wrong, not a transient
// fault — retry with backoff only on 5xx.
throw new InvalidOperationException(
$"Artham returned {(int)response.StatusCode}: {payload}");
}
return payload;
}
}
8. Sandbox & Testing
Integration Steps
- You share your sandbox base URL — e.g.
https://sandbox-api.yourbroker.com, plus the receiver URL where Artham should push executed trades (API 2). - Artham shares credentials — We generate and send you an API Key + Shared Secret over email.
- You build the API(s) — The Get Client Details endpoint (API 1) and the Executed Trades receiver (API 2).
- You build the daily holdings pull — a scheduled job that calls
GET /api/v1/broker/holdingsafter 07:00 IST and replaces your stored set (API 3). We will tell you when the endpoint is live on Sandbox. - End-to-end test — We call API 1 with test client codes, and POST test trade batches (NSE and BSE) to your API 2 receiver, verifying signature checks and idempotent dedupe. You call API 3 against Sandbox and we compare the snapshot against our books together.
- Go live — You share the production base URL and receiver URL. We generate a new set of production credentials.
Test Checklist
Authentication — every API
| Test Case | Expected Result |
|---|---|
| Valid API key + correct signature | 200 — client data on API 1, the snapshot on API 3 |
| Valid API key + wrong signature | 401 Unauthorized |
| Invalid / unknown API key | 401 Unauthorized |
Missing X-Api-Key or X-Signature headers | 401 Unauthorized |
API 1 & API 2 — the endpoints you host
| Test Case | Expected Result |
|---|---|
| Get details for an existing client | 200 with all required fields populated |
| Get details for a non-existent client code | 404 with CLIENT_NOT_FOUND |
| Get details for an inactive / suspended client | 422 with CLIENT_INACTIVE |
| POST a valid NSE trades batch | 2xx; response body ignored; trades stored under trades[] |
| Re-POST the same batch body (retry scenario) | 2xx; safe to reprocess — no duplicate rows created |
| POST NSE and BSE as two separate batches | Both accepted independently (up to 2 POSTs / broker / day) |
API 3 — the holdings pull you make
| Test Case | Expected Result |
|---|---|
| Self-test your HMAC against the demo secret in Section 6 | Digest matches e1ed253a…3204584a exactly — do this before your first call |
| Call with a valid signature | 200; flat data.holdings array |
Compare holdingCount and cashCount against their array lengths | Both equal — if not, the response was truncated; discard it |
Store a cash row and read it back | Amount preserved to 2 decimal places |
Store a client with a negative amount | Accepted — the column must be signed |
A client present in cash but not in holdings | Stored without error — the arrays are independent |
| Sign the full URL instead of the path (common mistake) | 401 with AUTH_TOKEN_INVALID — sign /api/v1/broker/holdings only |
Tamper with X-Signature | 401 with AUTH_TOKEN_INVALID |
| Omit both auth headers | 401 with AUTH_REQUIRED |
| A client holding a BSE-only scrip | Row has exchange: "BSE" and the numeric scrip code in symbol |
| A client who has fully exited every position | That clientCode is absent from the response entirely |
| Run the pull twice, then count your stored rows | Unchanged — the second snapshot replaces the first; merging would double the set |
Parse a quantity such as "12.5000" | Handled as a decimal, not an integer — see Section 6 |
Support
For integration questions, contact the Artham engineering team:
- Email: rahul@artham.co
- We are available for joint debugging calls during integration.