API reference · v1

The copydnn API

Fingerprint-first copy detection over HTTP. Every endpoint that takes media prefers a fingerprint computed where the media lives, so uploading is always optional. The story of how the system works, and a two-minute walkthrough, live on the landing page. This page is the contract. Every response on it is captured server output.

copydnn · one call to /v1 per line

    
The three verbs anyone starts with, and what comes back — each is one HTTP call: insert → POST /v1/references · query → POST /v1/queries · stream → POST /v1/streams. The full contract is below.
base url
This instance: http://127.0.0.1:5002, endpoints under /v1. Swap in your own host for a deployed instance. Opening /v1 in a browser lands on this page.
sending media
Three interchangeable shapes everywhere: raw .cdfp bytes (Content-Type: application/x-cdfp), a multipart file field, or JSON {"url": …} for the server to fetch.
compatibility
GET /v1/descriptor names this instance's descriptor space, e.g. {"model": "sscd/mixup", "dim": 512, "input_px": 224, "regions": 3, "signals": ["video","audio"]}. A fingerprint from another space is refused with a 409, never scored, since the score would be meaningless. The signals list advertises whether this instance also matches on audio; when it does, the audio fingerprint rides inside the same .cdfp as the video one — one artifact, two signals, no sidecar.
auth
A request with Authorization: Bearer cdnn_… (a personal token minted on your account page, shown once, revocable there) acts as you: your own library, your quota, your hourly limits. A request with no credentials lands in the shared guest sandbox with its pooled budgets — the same one the webpage offers visitors, where the oldest uploads are evicted when its disk cap fills. A presented token that is wrong is a 401, never a silent downgrade to the pool. GET /v1/me reports who you are and what you may spend; a spent hourly budget is a 429 with Retry-After, a full quota a 507. An instance started with --no-auth is open.
clients
curl works everywhere below, and the copydnn CLI speaks this API with the same verbs: copydnn --api http://host fingerprint|insert|query|compare|stream|list|rm.

00

Install & first search

Everything below assumes a running instance. One machine runs the engine and owns the library; clients talk to it with curl or the copydnn CLI, which ships with the engine and speaks this API when pointed at a host. From nothing to a first verdict is about two minutes plus the model download.

run an instance
python3 -m venv .venv
.venv/bin/pip install -e ".[web]"
.venv/bin/python scripts/fetch_models.py   # SSCD weights, ~99 MB, once
.venv/bin/copydnn-web                      # UI + this API on http://127.0.0.1:5002

# optional, for libraries past a few hundred thousand rows:
# python3 -m venv .venv-faiss && .venv-faiss/bin/pip install faiss-cpu numpy
first search
export COPYDNN_API=http://127.0.0.1:5002

copydnn fingerprint film.mp4     # a .cdfp beside it — kilobytes, media never moves
copydnn insert film.mp4.cdfp     # video:1  film.mp4  ready
copydnn query suspect.mp4
  STRONG  film.mp4  best 0.921
      query 11.2-38.8 -> reference 31.3-54.7  rate 1.000x

That is the whole loop: fingerprint where the media lives, insert the kilobytes, query anything suspicious. Everything the CLI just did is plain HTTP, documented endpoint by endpoint below.


01

Insert references

POST/v1/references

References are the videos and images you want to protect, your library. Insert them once and every future query is checked against them. There are three ways to hand us a reference, and the endpoint is the same for all of them.

You can attach your own tags: a metadata field holding a JSON object of string keys (rights holder, case id, ground truth). They are stored inside the fingerprint, come back on the reference detail, and are editable later with PATCH /v1/references/<id>/metadata.

Recommended

Send a fingerprint

Body is the .cdfp file. Private, kilobytes to megabytes, and indexed before the request returns. Its manifest names the source. Add ?name= to override.

Good middle ground

Send a link

Body is {"url": …}, typically a pre-signed storage link. We fetch it, fingerprint it, and keep the media alongside for evidence views.

Always works

Upload the file

Plain multipart upload, a file field, for when the other two are not an option.

you send
curl -X POST http://127.0.0.1:5002/v1/references \
  -H "Content-Type: application/x-cdfp" \
  --data-binary @film.mp4.cdfp

# or by link:  -H "Content-Type: application/json" \
#              -d '{"url": "https://storage…/film.mp4?sig=…"}'
# or upload:   -F "file=@film.mp4"
you get back · 201
{
  "id": 1,
  "kind": "video",
  "name": "bunny.mp4",
  "duration": 596.46,      // seconds
  "frames": 1012,         // distinct moments we kept
  "fingerprint_mb": 3.11,
  "status": "ready"       // searchable right now
}

Every insert is checked against the library first. If what you hand us is content it already holds (the same file, a re-encode, a mirrored copy under a new name), the insert is refused with 409 and the response names the reference it duplicates. Pass ?dedupe=false to insert it anyway.

That's all it takes. A ten-minute film becomes a thousand stored moments, searchable immediately. Remove one with DELETE/v1/references/1, list them all with GET/v1/references, inspect one with GET/v1/references/1.


02

Query

POST/v1/queries

Hand us a suspect clip, or its fingerprint, and ask what this system answers. Is this a copy of something in my library, and of which seconds, exactly?

The answer is a mapping between two timelines, with the evidence attached. Below, a 14-second mirrored excerpt is queried against the library that holds its source.

you send
curl -X POST http://127.0.0.1:5002/v1/queries \
  -H "Content-Type: application/x-cdfp" \
  --data-binary @clip.mp4.cdfp

# media works too, with a sampling knob:
#   -F "file=@clip.mp4" -F "fps=2"
you get back
{
  "qid": "fe510befae95",
  "query_id": "5b30c369967c",
  "url": "/queries/5b30c369967c",
  "query": "query.cdfp",   // a raw body's constant name; ?name= overrides
  "kind": "video",
  "created": 1785687866.0,
  "frames": 19,
  "duration": 13.33,
  "verdict": "strong",
  "timings": { "stage": "stored", "total": 0.0176 },
  "matches": [{
    "reference": 1,
    "name": "bunny.mp4",
    "kind": "video",
    "verdict": "strong",
    "score": 0.9268,
    "audio_only": false,
    "runs": [{
      "query": [0.0, 13.33],
      "reference": [119.67, 133.67],
      "rate": 1.0155,
      "offset": 119.92,
      "frames": 19,
      "mean_score": 0.9072,
      "modality": "video"
    }, {
      "query": [0.0, 13.1],
      "reference": [119.7, 132.8],
      "rate": 1.0,        // audio runs are rate-1.0 only
      "offset": 119.7,
      "frames": 26,
      "mean_score": 0.71,
      "modality": "audio"   // same reference, corroborated on the soundtrack
    }],
    "repeats": [ … ],
    "repetition": 0.0,
    "transform": []
  }, {
    "reference": 7,
    "name": "anthem.mp4",
    "kind": "video",
    "verdict": "strong",
    "score": 0.74,
    "audio_only": true,   // found on the soundtrack alone; no video run
    "runs": [{
      "query": [2.0, 11.4],
      "reference": [0.0, 9.4],
      "rate": 1.0,
      "offset": -2.0,
      "frames": 18,
      "mean_score": 0.74,
      "modality": "audio"
    }],
    "repeats": [],
    "repetition": 0.0,
    "transform": []
  }]
}

The run reads as a plain sentence. Seconds 0 to 13.3 of your clip are seconds 119.7 to 133.7 of bunny.mp4, supported by nineteen matched frames. The clip was flipped left-to-right, and the match still held.

verdict
The headline. One of strong, possible, or none, explained below.
score
Visual similarity of the best-matching moment, from 0 to 1. Above ~0.9 means near-identical frames.
runs
The evidence. Each run is one continuous stretch where your clip and the reference move together in time.
modality
Which signal carried a run: video (the SSCD frame fingerprint) or audio (the landmark constellation of the soundtrack). Both signals are searched on every query and fused into one result, so a reference can be supported by either or both. Absent on older results means video.
audio_only
true when a match has no video run at all — the copy was found only on its audio track. These clear a higher bar, since there is no picture to corroborate them.
signals
Add ?signals=video or ?signals=audio to narrow the response to one signal; the default both returns every run. Display-only: both signals are always computed and stored, so the same query re-read with a different value costs nothing. GET /v1/descriptor lists the signals this instance carries.
rate
The playback speed of the copy relative to the original. 1.0 is untouched, 1.5 sped up, 0.75 slowed down. Audio runs are always 1.0.
timings.stage
How the query was described. frames is the baseline, one whole-frame descriptor per sampled frame. frames+regions means region windows were searched too, which is what deep crops and insets need. stored and stored+regions are the same distinction for a .cdfp query, whose descriptors already exist. timings.views is the exact descriptors-per-frame this query paid for.
query_id
The clip's own address, taken from its content alone. The same bytes queried twice land on the same query_id, so GET /v1/queries/5b30c369967c re-opens the result any time. Add ?evidence=full for the raw similarity surfaces and per-frame pairs behind the verdict.
qid
This result's id: content plus name, sampling and settings. A changed threshold mints a new result under the same query_id, so you never get a stale verdict.
url
The same result as a page, at the query_id. Open it on the instance and the web UI renders the synced players, the dual timelines and the correlation heatmap for this exact query, the quickest way to see a match instead of reading it. Prefer ?evidence=full when you are drawing your own views instead.
strong

The timelines move together. Many frames agree on the same line through time. This is a copy.

possible

Some frames look right but the timeline evidence is thin. Worth a human glance, not a conclusion.

none

Nothing in your library explains this clip. Lookalikes score here too, which is the point.


03

Live streams

POST/v1/streams

Everything above answers a question about a file that has finished. A broadcast has not finished, and the question people actually ask of one is is my content on air right now — which has to be answered before it ends.

A stream is the same search, run on a window that keeps moving. Frames arrive, are embedded, and are appended to an accumulator that is a fingerprint at every moment. The rest follows from that: the live search is a .cdfp query over the last thirty seconds, closing the stream needs no recomputation because the descriptors the search was reading are already the artifact, and a stream you closed as a query can be promoted into the library later without decoding a frame twice.

Recommended

Push frames

You decode where the stream lands and send sampled frames as MJPEG. One ffmpeg pipe, no protocol for us to guess, and the traffic is the frames we look at rather than the whole broadcast.

Nothing leaves

Push descriptors

You embed as well, and send .cdfp batches. The same guarantee a file fingerprint makes, now for a live source: the pixels never cross the wire.

Least to run

Let us pull

Give us a source URL at open and this instance decodes it itself — HLS, RTMP, RTSP, UDP, plain HTTP. The only shape that needs the engine to reach your stream.

open, feed, close
# 1. open a session
curl -X POST http://127.0.0.1:5002/v1/streams \
  -H "Content-Type: application/json" \
  -d '{"mode":"query","name":"channel-4","fps":1,"record":true}'
#  -> {"id": "2e3ab1072bd9", "state": "live", ...}

# 2. feed it, forever if you like
ffmpeg -i "https://…/live.m3u8" -vf fps=1 \
       -f image2pipe -c:v mjpeg - \
  | curl -X POST --data-binary @- \
      -H "Content-Type: image/jpeg" \
      http://127.0.0.1:5002/v1/streams/2e3ab1072bd9/frames

# 3. end it. this is where it becomes permanent
curl -X POST http://127.0.0.1:5002/v1/streams/2e3ab1072bd9/close

# or all three at once, which is what the CLI is:
copydnn --api http://127.0.0.1:5002 stream "https://…/live.m3u8"
a push returns what it triggered
{
  "id": "559503d5d815",
  "state": "live",
  "duration": 13.0,       // seconds of stream so far
  "frames": 14,
  "searches": 1,
  "airings": 1,        // matches on air right now
  "events": [ { "type": "open", … },  // the backlog since ?since=0
  {
    "seq": 2,
    "type": "hit",
    "t": 13.0,
    "hit": {
      "n": 1,
      "reference": 1,
      "name": "bunny.mp4",
      "kind": "video",
      "verdict": "possible",  // firms as windows accumulate
      "score": 0.9834,
      "started": 0.0,
      "ended": 13.0,
      "reference_span": [33.67, 48.33],
      "ref_duration": 596.46,
      "rate": 1.1546,
      "offset": 33.21,
      "windows": 1,
      "open": true   // still on air
    }
  }]
}

One row per appearance, not one per search. A thirty-second window searched every five seconds sees the same match six times over. A monitor needs one row that grows, so consecutive windows landing on the same stretch of the same reference are folded into one airing and extended in place. It stays open while the content is on, and closing it is the event worth alerting on.

Airings are keyed on where the frames landed in the reference rather than on the fitted rate or offset, because a line fitted over one window is fitted over few frames and is unstable. The same thirteen-second clip produced consecutive windows at rates 1.033 and 0.681. Keyed on rate, that arrives as two airings of one continuous match. The rate an airing reports is taken from its best-supported window, which is why it converges as the match runs. The verdict firms the same way — a one-window airing often opens as possible before more windows harden it.

watch it happen · server-sent events
curl -N http://127.0.0.1:5002/v1/streams/2e3ab1072bd9/events

id: 2
event: hit
data: {"seq": 2, "type": "hit", "t": 13.0, "hit": {…}}

id: 3
event: update
data: {"seq": 3, "type": "update", "t": 18.0, "hit": {…}}

id: 9
event: end
data: {"seq": 9, "type": "end", "t": 96.0, "hit": {…}}

# reconnect with Last-Event-ID (or ?since=) and the
# backlog is replayed rather than lost.
closing · where it goes
curl -X POST http://127.0.0.1:5002/v1/streams/559503d5d815/close

{
  "state": "closed",
  "duration": 13.0,
  "frames": 14,
  "closed_as": {
    "kind": "query",
    "query": "caf28223430f",
    "result": "c4e3c9e0d276",
    "media": "media.mp4",
    "verdict": "possible",
    "url": "/queries/caf28223430f"
  },
  "hits": [{ "n": 1, "open": false, … }]
}
mode
What closing produces, and nothing else — both modes are searched live. query stores the stream in the query store with a full result against it, addressable and re-openable like any other. reference inserts it into the library, dedup check included.
record
Keep the sampled frames as a playable MP4. Off by default: descriptors alone detect everything, and this is the largest thing a stream writes. On, and the query or reference has media the UI can play, with proxy seconds equal to stream seconds so a hit at 04:12 is at 04:12 in the file.
fps · window · interval
Frames sampled per second of stream (the instance's query sampling rate unless set, accepted range 0.05–30), thirty seconds of context per search, a search every five. The window is what a temporal fit has to fit through, so shortening it costs confidence. The interval is how late a match can be reported, and may not exceed the window.
source
A URL for this instance to open itself (url is accepted as an alias). Present means we pull; absent means you push. Everything downstream is identical. At most four server-pull sessions may be live per library at once; a fifth open is refused with 429.
dedupe
Drop a frame that says nothing new against the last kept one. On for a reference stream, off for a query one, because every query frame is an independent vote for an alignment and throwing them away weakens the evidence.
a session survives its stream
The descriptors are checkpointed as they accumulate, so GET /v1/streams/{id}/fingerprint gives a searchable .cdfp for the first hour of a broadcast that has four to go, and a process that dies leaves what it had.

Both modes embed at the reference region layout, three concentric crops, which is what makes the accumulator ingestible. It is the same trade a stored .cdfp query already makes: less sensitive to a heavy crop or a picture-in-picture than a dropped clip searched with the region capabilities on, which can pay for fourteen windows and their mirrors per frame. A stream monitors continuously, so it buys the cheap version continuously. Watch the whole broadcast this way, then re-query the interesting minutes as a file with crops and insets enabled.


04

Compare two files

POST/v1/compare

Sometimes there is no library, just two files and one question. Compare takes two fingerprints (or media files), answers in the same verdict-and-runs shape, and stores nothing.

one call, in and out
curl -X POST http://127.0.0.1:5002/v1/compare \
  -F "a=@clip.mp4.cdfp" -F "b=@film.mp4.cdfp"

{ "verdict": "strong", "score": 0.9919, "runs": [ … ] }

When something goes wrong

Errors are sentences

Similarity math fails silently. Two incompatible fingerprints would still produce a number, just a meaningless one. So this API rejects the mismatch instead, and every refusal says what happened and what to do about it. All three bodies below are verbatim server output.

409 · wrong descriptor space
{
  "error": "cannot ingest clip.mp4.cdfp: descriptor width
           differs: fingerprint has 256, library uses 512",
  "hint": "re-run the fingerprinter with a client
           matching GET /v1/descriptor"
}
400 · not a fingerprint at all
{
  "error": "request body is not a .cdfp fingerprint
           (bad container)"
}
409 · already in the library
{
  "error": "duplicate of video:1 bunny.mp4 (covers 100% of it)",
  "duplicate": { "ref": 1, "name": "bunny.mp4", "kind": "video",
                 "score": 0.998, "coverage": 1.0 },
  "hint": "POST again with ?dedupe=false to insert anyway"
}
401 · a presented token is wrong
# a wrong token is refused, never downgraded to the
# guest pool. With no credentials at all (and guests
# off) the error is "authentication required".
{
  "error": "unknown API token",
  "hint": "check the key, or mint a new one on your
           account page"
}
429 · this hour's budget is spent
# also sends Retry-After: 840
{
  "error": "hourly query limit reached: 500000 of 500000
           embeddings used",
  "used": 500000, "limit": 500000, "resets_in": 840,
  "hint": "retry after the reset, or ask an admin to raise
           the limit"
}
507 · a storage quota is full
# scope is "user" (your quota) or "instance" (the whole
# data folder). Hourly budgets reset; storage does not --
# something has to be deleted, or the quota raised.
{
  "error": "your storage quota is full: 9.9 GB of 10.0 GB used",
  "scope": "user", "used": 9902341120, "limit": 10000000000,
  "hint": "delete references, queries or streams you no longer
           need, or ask an admin to raise your quota"
}

The whole surface

Endpoint index

Everything above, in one glance. If an endpoint is not on this list, it does not exist yet.

MethodPathWhat it does
GET/v1/meWho this token is: identity, hourly limits, and this hour's usage. The self-report a script wants before a long run.
GET/v1/descriptorThe server's fingerprint dialect, so clients can check compatibility first.
POST/v1/referencesInsert into your library. Accepts a fingerprint body, a link, or a file.
GET/v1/referencesList your library.
GET/v1/references/{id}One reference in detail.
GET/v1/references/{id}/fingerprintDownload the reference's portable .cdfp — everything needed to search for it elsewhere, without the media.
PATCH/v1/references/{id}/metadataMerge-patch user metadata; a null value deletes a key. Tags live in the fingerprint and travel with it.
DEL/v1/references/{id}Remove a reference and its stored moments.
POST/v1/queriesQuery the library. The core of the API.
GET/v1/queries/{query_id}Re-open a saved result at the query's permanent address.
POST/v1/streamsOpen a live session. Nothing is searched until frames arrive.
POST/v1/streams/{id}/framesFeed a session. MJPEG bytes, multipart images, or a .cdfp descriptor batch.
GET/v1/streams/{id}/eventsServer-sent events. Every airing the moment the engine forms it.
POST/v1/streams/{id}/closeEnd the stream and materialise it. ?keep=false ends it without keeping anything.
GET/v1/streamsEvery session, newest first, with how many are live. Paginated: ?page=, ?per_page= (48 by default, up to 500), ?filter= one of all · live · kept · empty · failed, ?q= a name or key:value tag, ?sort=/?direction=. Counts for every filter ride along.
GET/v1/streams/{id}One session and its airings, live or long finished. ?evidence=full adds each airing's per-frame (stream s, reference s, score) triples.
GET/v1/streams/{id}/posterThe most recent frame sampled, as a jpeg. A recording only plays once the stream ends, so this is what a live view shows.
GET/v1/streams/{id}/fingerprintThe descriptors so far as a portable .cdfp. Valid mid-flight, because the accumulator is a fingerprint at every moment.
DEL/v1/streams/{id}Stop a session and remove everything it produced.
POST/v1/compareTwo files, one verdict, nothing stored.

The UI's own backend, for completeness

The web UI talks to a private surface under /api/* on this same server. It answers in the shapes the page was built around and is free to change with the page — integrate against /v1 above. It authenticates like everything else, cookie or bearer, with two extra guards: cookie-authed writes are refused when the request's Origin does not match the host, and the credential-managing routes insist on a session cookie, so a leaked API token can never mint credentials. The complete surface, grouped by what it serves:

MethodPathWhat it does
library
GET/api/stats · /api/items · /api/item/<id>Library totals, the reference listing (with metadata), and one reference in full detail.
GET/api/thumb/<id> · /api/media/<id>A poster frame at ?t=, and the seekable media itself.
POST/api/addInsert an upload, with dedup, progress, and an optional ingest profile.
DELETE/api/items/<id>Remove a reference.
POST/api/items/<id>/reingestRe-fingerprint one reference, optionally under an ingest profile; provenance recorded.
PATCH/api/items/<id>/metadata · /api/items/metadataMerge-patch one reference's metadata, or many in one call.
fingerprints
GET/api/items/<id>/fingerprintThe fingerprint view: dimensions, flat flags, novelty curve, compatibility.
GET/api/items/<id>/fingerprint/downloadThe .cdfp itself, named after the reference.
POST/api/fingerprint/inspectInspect an uploaded .cdfp without ingesting it.
search
POST/api/query · /api/query_item/<id> · /api/query_exampleSearch with an upload, a stored reference's own vectors, or a packaged demo clip.
GET/api/queries/<qid>/results/<rid>One stored reading in full. The list of readings rides on GET /api/queries/<qid> as results[]; a result belongs to the query it came from, so it is deleted when that query is.
GET/api/items/<id>/resultThe same, for a reference searched against the library — a reference is its own query. ?result= or ?query_mode= picks a reading.
POST/api/queries/<qid>/runRun a stored query again, optionally under a search profile.
DELETE/api/queries/<qid>/results[/<rid>] · /api/items/<id>/results[/<rid>]Forget every reading of a query or reference, or one of them.
GET/api/progress/<token> · /api/hero · /api/examples · /api/countsLive progress for a running request, the landing page's surfaces, and how many references and queries are held.
queries
GET/api/queries · /api/queries/<qid>The paginated grid (filters, counts, key:value search) and one query with its reading.
POST/api/queriesAdd a file to the query store — and answer it. Every item in the store has a result. Without one, it is still just a file.
GET/api/queries/<qid>/thumb · /api/queries/<qid>/mediaPoster and seekable media for a query.
POST/api/queries/<qid>/promotePromote a query into the library; metadata carries over, and the query is left as a tombstone.
PATCH/api/queries/<qid>/metadata · /api/queries/metadataMerge-patch one query's metadata, or many in one call.
DELETE/api/queries/<qid>Delete a query, and with it every result about it.
playback
GET/api/media/<id>/codec · /api/queries/<qid>/codecWhat a stored file actually is, and which of three tiers will play it: the browser's own decoder, the WebAssembly one in the page, or a transcode.
clusters
GET/api/clustersThe map of how the library duplicates itself: nodes, labelled edges, and the groups they form. Served whole, because the page re-clusters as thresholds move.
POST/api/clusters/buildRebuild the map — one cached search per reference. Runs in the background; one at a time.
GET/api/clusters/statusProgress of a running build.
DELETE/api/clustersClear the map. Only the map — the searches behind it belong to their references.
GET/api/refs/<id>/relatedOne reference's edges, for its detail page.
accounts & auth
POST/auth/login · /auth/logout · /auth/logout_allSign a session cookie in and out. logout_all revokes every session of the account at once.
GET/api/me · /api/me/usage · /api/me/tokensWho is calling, with limits, hourly usage and disk; the usage ledger alone (?hours=, up to 720); the token list — never the secrets.
POST/api/me/password · /api/me/tokensChange the password (signs out every other session), or mint an API token — the secret appears once, in this response, and never again.
DELETE/api/me/tokens/<tid>Revoke a token.
configuration & admin
GET/api/settings · /api/profilesThe settings registry with current values, and both profile sets.
PUT/api/settings · /api/profilesApply setting overrides, or save one kind's profile set — both validated against the registry.
DELETE/api/settingsReset every override to the shipped defaults.
POST/api/reingest · /api/admin/clearRe-fingerprint the whole library under current settings, or empty exactly one store (queries, references, or results).
GET/api/admin/users · /api/admin/instance · /api/admin/guest · /api/admin/usageThe admin panel, admins only: every account with usage and disk, the instance-wide disk cap, the guest pool's state, and per-user usage history.
POST/api/admin/users · /api/admin/guest/clear · /api/admin/guest/clusters/buildCreate an account (its one-time password is in the response), empty the guest pool of uploads, or rebuild the sandbox's cluster map.
PATCH/api/admin/users/<uid> · /api/admin/instance · /api/admin/guestOne account's limits, active flag, admin flag or password reset; the instance disk cap; the guest pool's budgets.
DELETE/api/admin/users/<uid>Delete an account. ?purge=1 with {"confirm": "<email>"} removes its data too.