
ID types in the Merkl API
A reference for the different IDs used across the Merkl API endpoints — campaigns, opportunities, tokens, chains, protocols.
The Merkl API uses several distinct ID types. Many of them are stringified numbers and look interchangeable, but they aren't: passing one where another is expected typically returns an empty array ([]) silently. This page explains each ID type, how to recognize it, and which endpoints accept which.
TL;DR table
| ID type | Format | Example | Used as |
|---|---|---|---|
Internal campaign ID (Campaign.id) | numeric hash, ≤20 digits | 13896840291241714382 | Response field campaignId (in records, breakdowns), query ?id=, ?rootCampaignId=, ?parentCampaignId= |
Onchain campaign ID (Campaign.campaignId) | 32-byte hex, 0x… | 0x93cf385c2a446a7596820601caf25e63bfc4939efa9a89a6f21563b58ee54eb1 | Response field onChainCampaignId, query ?campaignId=, ?campaignIds=, path /rewards/campaign/:campaignId/list |
Internal opportunity ID (Opportunity.id) | numeric hash, ≤20 digits | 7673870997270712040 | Response field id, query ?opportunityId=, ?id=, path /opportunities/:id |
| Composite opportunity ID (path-only) | {chainId}-{type}-{identifier} | 1-ERC20LOGPROCESSOR-0x35Cbe8542E70fa2f7F9cDF129F19e593F4b4f560 | Path /opportunities/:id only, as an alternative to the numeric form. Not accepted in query parameters. |
Opportunity identifier (mainParameter) | free-form, often an EVM address | 0x35Cbe8542E70fa2f7F9cDF129F19e593F4b4f560 | Query ?identifier=, ?mainParameter=, ?explorerAddress= |
Token ID (Token.id) | numeric hash, ≤20 digits | 3460729690457214808 | Response field id on token objects, and underlyingTokenId on a wrapper; query ?id=, path /tokens/:id |
| Token shorthand (path-only) | {chainId}-{address} | 1-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | Path /tokens/:id and /tokens/:id/allowance/... only. Server resolves it to the numeric Token.id. Not accepted in ?id= query. |
| Chain ID | small integer | 1 (Ethereum), 42161 (Arbitrum) | Query ?chainId=, path /chains/:chainId |
| Protocol slug | lowercase string | aave, morpho, uniswap | Query ?mainProtocolId=, path /protocols/:id |
| EVM address | 20-byte hex, 0x… | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | Query ?address=, ?explorerAddress=, ?recipient=, ?creatorAddress= |
The two campaign IDs
The single most common source of confusion: the field called campaignId in API responses is not what the ?campaignId= query parameter expects.
A campaign object has both:
{
"id": "13896840291241714382",
"campaignId": "0x93cf385c2a446a7596820601caf25e63bfc4939efa9a89a6f21563b58ee54eb1",
"...": "..."
}
| Field in response | DB column | What it is |
|---|---|---|
id | Campaign.id | Internal numeric hash, derived from Hash(distributionChainId-campaignId). Stable per chain. |
campaignId | Campaign.campaignId | Onchain campaign hash emitted by the distributor contract. The same value can exist on multiple chains. |
Some response shapes (e.g. rewardsRecord.breakdowns) project Campaign.id under a field also named campaignId, with onChainCampaignId for the hex hash. When in doubt, look at the format: 32-byte 0x… is onchain, ≤20-digit numeric is internal.
Which endpoints take which?
GET /v4/opportunities?campaignId=→ onchain hash (0x…)GET /v4/campaigns?campaignId=→ onchain hash (0x…)GET /v4/campaigns?id=→ internal numeric IDGET /v4/rewards/campaigns?campaignId=→ onchain hash (paired withchainId)GET /v4/rewards/campaign/:campaignId/list→ onchain hashGET /v4/claims?campaignId=→ onchain hash
Path-only forms
Two ID types are accepted only in path parameters, never in query filters. They exist as ergonomic shortcuts so you can hit /v4/opportunities/:id or /v4/tokens/:id without first looking up the numeric ID.
Composite opportunity ID
{chainId}-{type}-{identifier}, e.g. 1-ERC20LOGPROCESSOR-0x35Cbe8542E70fa2f7F9cDF129F19e593F4b4f560.
chainId: numeric chain id (1,42161, …)type: opportunity type, either the enum name (ERC20LOGPROCESSOR,CLAMM,AAVE_NET_LENDING, …) or its numeric valueidentifier: the opportunity'smainParameter(often a pool/vault/market address)
# Both resolve to the same opportunity
GET https://api.merkl.xyz/v4/opportunities/7673870997270712040
GET https://api.merkl.xyz/v4/opportunities/1-ERC20LOGPROCESSOR-0x35Cbe8542E70fa2f7F9cDF129F19e593F4b4f560
The composite form is not accepted in ?id= or any other query filter, only on the /opportunities/:id path.
Token shorthand
{chainId}-{address}, e.g. 1-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48. The server splits on -, looks up the token, and resolves to its canonical numeric Token.id.
# Both resolve to the same token
GET https://api.merkl.xyz/v4/tokens/3460729690457214808
GET https://api.merkl.xyz/v4/tokens/1-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Like the composite opportunity ID, this shorthand only works on path parameters (/tokens/:id, /tokens/:id/allowance/...). The ?id= query expects the canonical numeric form (and accepts a comma-separated list).
Both forms read the token index. An address Merkl has never indexed is a 404, whichever form you use.
Resolving a wrapper's underlying token
A Merkl token wrapper pays out a different token on claim. Every token payload carries the link as underlyingTokenId, a canonical Token.id:
{
"id": "8152336397138133356",
"symbol": "WXPL",
"address": "0x461B1d4ae26B4f4CB627c18aD85F223Ef8a0dd18",
"underlyingTokenId": "9177007823639843523"
}
The field appears wherever a token does — on rewardToken in /v4/campaigns, on the tokens in /v4/opportunities, on /v4/tokens/reward, and on user rewards. It is absent when the token is not a wrapper.
Token.id is a hash, so the address cannot be derived from it. Resolve it with a second call:
GET https://api.merkl.xyz/v4/tokens/9177007823639843523
{
"id": "9177007823639843523",
"symbol": "WXPL",
"address": "0x6100E367285b01F48D07953803A2d8dCA5D19873",
"decimals": 18,
"chainId": 9745
}
Or resolve both in one call with returnUnderlying:
GET https://api.merkl.xyz/v4/tokens/8152336397138133356?returnUnderlying=true
{
"id": "8152336397138133356",
"symbol": "WXPL",
"underlyingTokenId": "9177007823639843523",
"underlying": { "id": "9177007823639843523", "address": "0x6100E367285b01F48D07953803A2d8dCA5D19873" }
}
underlying is null when the token is not a wrapper.
Resolve several at once with the query form, which takes the canonical numeric ids only:
GET https://api.merkl.xyz/v4/tokens?id=9177007823639843523&id=3460729690457214808
Neither route needs an API key or campaign ownership. /v4/tokens/wrappers answers a different question — every wrapper one creator distributes — and needs a creatorId or creatorAddress.
Looking up an opportunity from a campaign
If a customer hands you a campaign hash and you want the matching opportunity, the most reliable path is:
# By onchain campaign ID
https://api.merkl.xyz/v4/opportunities?campaignId=0x93cf385c2a446a7596820601caf25e63bfc4939efa9a89a6f21563b58ee54eb1
# Or by the underlying pool/vault/market address (often easier)
https://api.merkl.xyz/v4/opportunities?explorerAddress=0x35Cbe8542E70fa2f7F9cDF129F19e593F4b4f560
Once you have an opportunity, you have its id (internal), which can be used directly:
https://api.merkl.xyz/v4/opportunities/7673870997270712040
Side-by-side example
A single opportunity response surfaces every ID type at once. Here is an annotated excerpt:
{
"id": "7673870997270712040", // internal Opportunity.id (use with /opportunities/:id)
"chainId": 1, // chain ID (Ethereum)
"type": "ERC20LOGPROCESSOR",
"identifier": "0x35Cbe8542E70fa2f7F9cDF129F19e593F4b4f560", // mainParameter, the vault address here
"explorerAddress": "0x35Cbe8542E70fa2f7F9cDF129F19e593F4b4f560",
"protocol": { "id": "morpho" }, // protocol slug, NOT a numeric ID
"tokens": [
{
"id": "3460729690457214808", // internal Token.id
"chainId": 1,
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"symbol": "USDC"
}
],
"rewardsRecord": {
"breakdowns": [
{
"campaignId": "13896840291241714382", // internal Campaign.id (numeric hash)
"onChainCampaignId": "0x93cf385c2a446a7596820601caf25e63bfc4939efa9a89a6f21563b58ee54eb1",
"id": "26e95ebc-b90e-414e-84ed-acacc424faf2" // breakdown row UUID, internal
}
]
}
}
Quick sanity rules
- A 32-byte hex string (
0x+ 64 hex chars) is an onchain campaign ID. - A 20-byte hex string (
0x+ 40 hex chars) is an EVM address (token, pool, vault, or recipient). - A 1-20 digit decimal string is an internal hash (campaign, opportunity, or token, depending on context).
- A small integer (1, 137, 42161, …) is a chain ID.
- A lowercase word (
aave,morpho) is a protocol slug.
If a query returns [], double-check the ID format against this table before assuming the data isn't there.