Artham
ARTHAM

Broker Integration API Specification

Version 1.3  ·  September 2026

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

  1. Artham calls your APIs — you build the endpoints described in this document.
  2. You verify our identity — using the API key + HMAC signature we send with every request.
  3. You return JSON responses — in the standard format described below.
  4. 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

#MethodEndpointPurpose
1GET/api/v1/clients/{ppCode}/{clientCode}Fetch client KYC and account detailsRequired
2POST{your-trades-receiver-URL}Receive the daily batch of executed trades (fills) — Artham pushes, you hostRequired
3GET/api/v1/broker/holdingsPull all your clients' equity holdings — Artham hosts, you callRequired

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
All traffic is HTTPS over TLS 1.2+, in both directions. Artham does not restrict caller IPs on API 3. If you allowlist on your side for API 1 and API 2, ask us for our egress IPs.

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:

HeaderDescriptionExample
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

  1. Artham generates an API Key (public identifier) and a Shared Secret (private, used for HMAC) for each broker.
  2. Artham will share both credentials with you over email.
  3. 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).
One credential pair per broker. You will receive a single API Key + Shared Secret. It is used across all advisors and clients managed through Artham, and in both directions — the same pair verifies our calls to you and signs your calls to us. You do not need per-advisor, per-client or per-direction keys.

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
ComponentDescriptionExample
HTTP_METHODUppercase HTTP methodGET
pathRequest path (no query string, no host)/api/v1/clients/PP001/AB1234
bodyRaw JSON request body. Empty string "" for GET requests."" (GET has no body)
Note: For GET requests with no body, the message ends with an empty string: "GET\n/api/v1/clients/PP001/AB1234\n"

Verifying — Artham → You

  1. Check API key — look up the X-Api-Key header value. Reject with 401 if it doesn't match the key Artham shared with you.
  2. Recompute signature — using the Shared Secret, compute the HMAC-SHA256 of the same message format shown above.
  3. Compare — use a constant-time comparison. Reject with 401 if signatures don't match.

Signing — You → Artham

  1. Build the message"{METHOD}\n{path}\n{body}", exactly as above. API 3 is a GET with no body, so the message is "GET\n/api/v1/broker/holdings\n".
  2. Compute the HMAC — HMAC-SHA256 of that message with your Shared Secret, as a lowercase hex string.
  3. Send both headersX-Api-Key (your key, unchanged) and X-Signature (the hex digest). Artham runs the same three verification steps in reverse and answers 401 if either fails.
Sign the path only — no host, no query string. The message uses /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

StatusWhen to Use
200Success
400Bad request (invalid params, missing fields)
401Invalid API key or signature
404Client not found
422Business rule violation (insufficient limit, etc.)
500Internal server error

Error Codes

Use these standard error codes in the error.code field:

CodeDescription
CLIENT_NOT_FOUNDThe given clientCode does not exist in your system
CLIENT_INACTIVEClient account is deactivated or suspended
INSUFFICIENT_LIMITClient does not have enough available limit/balance
INVALID_REQUESTMissing or malformed request parameters
INTERNAL_ERRORUnexpected 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.

GET /api/v1/clients/{ppCode}/{clientCode} Fetch client KYC and account details

Path Parameters

ParameterTypeDescription
ppCodestringThe portfolio provider code assigned to your brokerage
clientCodestringThe 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.

HeaderRequiredDescriptionExample
X-Api-KeyYesStatic API key identifying Artham.artham_live_k8Fj2mNp...
X-SignatureYesLowercase 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

FieldTypeRequiredDescription
clientCodestringYesClient's unique trading code at your brokerage
rmCodestringYesRelationship Manager code assigned to this client
namestringYesFull name as registered in your system
panstringYesPAN number (10 characters, e.g. ABCDE1234F)
dateOfBirthstringYesDate of birth in YYYY-MM-DD format
address1stringNoAddress line 1
address2stringNoAddress line 2
address3stringNoAddress line 3
citystringNoCity
statestringNoState
pinCodestringNoPIN code (6 digits)
emailAddressstringYesRegistered email address
mobileNumberstringYesRegistered mobile number (10 digits)
accountNostringYesClient's primary bank account number
ifscCodestringYesIFSC code of the bank branch (11 characters)
accountTypestringYesBank account type: "S" (Savings), "C" (Current), or "O" (Other)
clientTypeCodeintegerYesClient constitution code. Return one of the supported values listed below.
isNRIbooleanYestrue if the client is a Non-Resident Indian
isPoliticallyExposedPersonbooleanYestrue if the client is a Politically Exposed Person (PEP)
Data accuracy: The data returned by this API is used for KYC verification, agreement generation, and regulatory compliance. Please ensure all fields — especially pan, name, dateOfBirth, and bank account details — are accurate and match your KYC records.

Field Details

accountType values:

ValueDescription
"S"Savings account
"C"Current account
"O"Other

clientTypeCode values:

ValueDescription
1Individual
2Mutual Fund
3Body Corporate
4Non-Tax Paying Entity
5Others
6Hindu Undivided Family
7Oversees Corporate Body
8Partnership Firm
9Merchant Banker
10Foreign Institutional Investor
11Indian Financial Instituion
12Banks
13Company
14Trust
15Financial Institution
16NBFC
17Society
18NRI
19Statutory Bodies
20Insurance Companies
21Proprietor
22National Pension Scheme
23Depository Receipts
24Foreign Direct Investments
25Foreign Venture Capital Funds
26Non Govt. Organisation
27PMS IND
28PMS NON-IND
29QFI IND
30QFI Others
31LIMITED LIABILITY PARTNERSHIPS
32Non-Banking Financial Company (NBFC)
33Alternate Investment Fund
34Foreign National
35Domestic Venture Capital Fund
36FPI 1 (INS)
37FPI 2 (INS)
38FPI 3 (INS)
39FPI 1 (I)
40FPI 2 (I)
41FPI 3 (I)
42FPI 1 (NI)
43FPI 2 (NI)
44FPI 3 (NI)
71NRO

If your system uses different codes, please share your mapping and we will align.

Error Responses

HTTPError CodeWhen
401Invalid API key or signature
404CLIENT_NOT_FOUNDNo client exists with this clientCode
422CLIENT_INACTIVEClient 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.

Direction: Artham → your endpoint. You provide the receiver URL (separate Sandbox and Production URLs). NSE and BSE are sent as separate batches — up to two POSTs per broker per day. When there are no fills, no request is sent (never an empty batch).
POST {your-trades-receiver-URL} Executed trades batch (NSE / BSE)

Endpoint & Method

ItemValue
MethodPOST
URLYou host it — send Artham your Sandbox and Production receiver URLs
TLSTLS 1.2+ over HTTPS
IP allowlistingNo 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.

HeaderDescriptionExample
X-Api-KeyStatic API key identifying Artham (see Section 2).artham_live_k8Fj2mNp...
X-SignatureHMAC-SHA256 of the request, computed over the raw body (see Section 2).a1b2c3d4... (64 hex)
Content-TypeAlways 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.

#KeyTypeDescription & format
1clientCodestringClient / UCC code
2exchangestring"NSE" or "BSE"
3segmentstring"CM" (Cash Market) or "FO" (Futures & Options)
4scripNamestringOMS symbol
5transactionTypestringTrade side — "BUY" or "SELL"
6isinstring12-char ISIN, e.g. "INE467B01029"
7quantitystringPositive integer as a string, e.g. "10"
8pricestringTrade Price
9productTypestringOne of "Intraday", "Delivery", "MTF", "Normal Carry Forward"
10orderTypestringOne of "Market" or "Limit"
11exchangeOrderNumberstringExchange order number
12timestampstringIST string, e.g. "27 FEB 2026 14:36:49"
Formats to note. All numbers (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"
}
FieldTypeDescription
statusbooleantrue if the batch was accepted, false if it was rejected
codeintegerApplication status code — 200 on success
messagestringHuman-readable result message

Retries & Resilience Artham is the caller

How Artham reacts to the HTTP status your endpoint returns:

Status your endpoint returnsWhat it meansArtham's action
2xx — e.g. 200 OKSuccess — batch received and acceptedDone. Never re-sent.
408 Request TimeoutYou didn't finish reading / processing the request in timeRetry
429 Too Many RequestsYou're rate-limiting usRetry (with backoff)
5xx500, 502, 503, 504Server error on your side — usually transientRetry
Network error / timeoutNo HTTP response at all (connection drop, DNS / TLS failure, read timeout)Retry
Any other 4xx400, 401, 403, 404, 422Client error — the request is rejected as-is; retrying won't helpStop. No retry. Please share sample error bodies.
BehaviourDetail
Re-deliveryRetries 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 policyUp to 5 attempts, exponential backoff (base 60 s). A reconciler also sweeps stale rows every 5 min.
Timeout15 s per attempt (configurable); your 30 s is fine.
Rate limitsNone 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.

Direction — this one is reversed. APIs 1 and 2 are Artham calling you. API 3 is you calling Artham. Authentication is the same 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.
Positions and cash — nothing else. The response carries equity positions and each client's cash balance, in two separate arrays. Individual ledger movements, NAV and fee breakdowns stay out of scope; the cash figure is a single balance per client, not a statement.
GET /api/v1/broker/holdings All clients' equity holdings and cash, one response

Endpoint & Method

ItemValue
MethodGET
Path/api/v1/broker/holdings
Base URLArtham-hosted — see Section 1
AuthenticationX-Api-Key + X-Signature, signed by you — see Section 2
Query parametersNone. There is no date, client or exchange filter — the response is always the current snapshot for all of your clients.
PaginationNone. The full set is returned in one response.
Your brokerage is identified by your API key, not by a parameter. There is no BrokerId in the request — the key resolves to your brokerage on our side, and can only ever return your own clients' positions.

Headers

HeaderRequiredDescription
X-Api-KeyYesThe API key Artham issued you — the same one you use to recognise our calls on API 1 and API 2.
X-SignatureYesLowercase 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>"
This signature never changes. The method, path and (empty) body are fixed, so the digest is constant for your secret — you may compute it once and cache it in your scheduler rather than recomputing it daily. Compute it from the message rather than copying it by hand, so a future path change does not silently break the call.

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

FieldTypeDescription
successbooleantrue on success — the standard envelope from Section 3
errorobjectnull on success; on failure carries code and message
data.asOfstringDate 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.clientCountintegerNumber of distinct clientCode values across both arrays — a client can appear in one without the other
data.holdingCountintegerNumber of rows in holdings. Use it to confirm nothing was truncated in transit.
data.cashCountintegerNumber of rows in cash. Same purpose.
data.holdingsarrayFlat array of holding rows — see below. Empty array if you have no clients with positions.
data.casharrayOne 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.

#KeyTypeDescription & format
1clientCodestringClient / UCC code — the same code used in API 1 and API 2
2isinstring or null12-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).
3symbolstringTrading 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".
4exchangestring"NSE" or "BSE"
5quantitystringNet 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).
Parse 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.

#KeyTypeDescription & format
1clientCodestringClient / UCC code — joins to the same code in holdings
2amountstringCash balance in rupees, as a decimal string with 2 decimal places — e.g. "10018.67". Can be negative (see below).
A cash balance can be negative. A debit balance is a real state — charges applied against an account that has been drawn down, for instance — and it is reported as a negative amount rather than clamped to zero. A parser that assumes a non-negative value, or a column typed as unsigned, will fail the first time one occurs.
The two arrays are independent. A client can appear in 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.

RuleDetail
Equity onlyMutual fund and bond positions are excluded — they have no exchange trading symbol, so symbol / exchange would be meaningless.
Positive quantities onlyRows with quantity of zero or less are omitted. A position the client has fully exited simply disappears from the response.
Aggregated per clientA 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 onlyA 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 derivedArtham 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 onlyScoped to clients whose broker mapping on Artham is you. Clients of other brokers are never returned.
Replace, do not merge. Each response is a complete snapshot, not a delta. Overwrite your stored set — both arrays — on every successful call. If you merge instead, exited positions and stale balances will live in your database forever, because a closed position and a zeroed balance are each expressed by their absence from the response.

Timing & Scheduling

Call once per day, after 07:00 IST. Artham's overnight processing — corporate actions included — completes before then. An earlier pull can return quantities that are superseded the same morning, and your stored copy will silently disagree with ours until the next day's call.
BehaviourDetail
FrequencyOnce 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 / holidaysReturns the last completed business day's positions. Safe to call daily.
RetriesOn 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."
  }
}
HTTPerror.codeWhenWhat to do
401AUTH_REQUIREDX-Api-Key or X-Signature missingSend both headers
401AUTH_TOKEN_INVALIDUnknown API key, or the signature does not match the message we recomputedCheck you signed the path only and included the trailing newline for the empty body
500INTERNAL_ERRORUnexpected error on Artham's sideRetry with backoff
Branch on the HTTP status, not on the error string. The 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.

Signing and verifying are the same computation. Both groups build the identical "{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

  1. 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).
  2. Artham shares credentials — We generate and send you an API Key + Shared Secret over email.
  3. You build the API(s) — The Get Client Details endpoint (API 1) and the Executed Trades receiver (API 2).
  4. You build the daily holdings pull — a scheduled job that calls GET /api/v1/broker/holdings after 07:00 IST and replaces your stored set (API 3). We will tell you when the endpoint is live on Sandbox.
  5. 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.
  6. 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 CaseExpected Result
Valid API key + correct signature200 — client data on API 1, the snapshot on API 3
Valid API key + wrong signature401 Unauthorized
Invalid / unknown API key401 Unauthorized
Missing X-Api-Key or X-Signature headers401 Unauthorized

API 1 & API 2 — the endpoints you host

Test CaseExpected Result
Get details for an existing client200 with all required fields populated
Get details for a non-existent client code404 with CLIENT_NOT_FOUND
Get details for an inactive / suspended client422 with CLIENT_INACTIVE
POST a valid NSE trades batch2xx; 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 batchesBoth accepted independently (up to 2 POSTs / broker / day)

API 3 — the holdings pull you make

Test CaseExpected Result
Self-test your HMAC against the demo secret in Section 6Digest matches e1ed253a…3204584a exactly — do this before your first call
Call with a valid signature200; flat data.holdings array
Compare holdingCount and cashCount against their array lengthsBoth equal — if not, the response was truncated; discard it
Store a cash row and read it backAmount preserved to 2 decimal places
Store a client with a negative amountAccepted — the column must be signed
A client present in cash but not in holdingsStored 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-Signature401 with AUTH_TOKEN_INVALID
Omit both auth headers401 with AUTH_REQUIRED
A client holding a BSE-only scripRow has exchange: "BSE" and the numeric scrip code in symbol
A client who has fully exited every positionThat clientCode is absent from the response entirely
Run the pull twice, then count your stored rowsUnchanged — 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: