Misi.Link

Documentazione dell’API

Invia file dai tuoi strumenti (script, programmi, server): crea un trasferimento, carica i file a pezzi, ottieni il link.

Chiave disponibile con i piani Pro e Business, in Il mio account › API. Prezzi

Autenticazione

Crea una chiave in Il mio account › API, poi inviala nell’intestazione Authorization di ogni richiesta. Tienila segreta: dà accesso ai tuoi trasferimenti. Una chiave eliminata smette subito di funzionare.

Authorization: Bearer mlk_…

Se il tuo strumento o il tuo hosting non trasmette l’intestazione Authorization, invia la stessa chiave nell’intestazione X-API-Key:

X-API-Key: mlk_…

120 richieste al minuto per chiave. Dimensione, numero di file e durata: quelli del tuo piano.

Inviare file in tre passaggi

1. Creare il trasferimento

Indica l’elenco dei file (nome e dimensione in byte). Con «recipients», ogni destinatario riceve un’e-mail; senza destinatari, ottieni un link da condividere. Facoltativi: «message», «password», «expire_days», «delete_after_download».

curl -X POST https://misi.link/api/v1/transfers \
  -H "Authorization: Bearer $MISILINK_KEY" -H "Content-Type: application/json" \
  -d '{"files": [{"name": "film.mov", "size": 5368709120}],
       "recipients": ["client@example.com"], "message": "…", "expire_days": 14}'
{"ok": true, "id": "aB3dE5fG", "token": "…", "url": "https://misi.link/aB3dE5fG",
 "files": [{"id": 101, "name": "film.mov", "size": 5368709120}], "chunk_size": 8388608}

2. Caricare i file

Invia ogni file a pezzi di al massimo «chunk_size» byte, in ordine: «offset» è il numero di byte già ricevuti per quel file. Dopo un’interruzione, «status» indica da dove riprendere (risposta 409: riprendi da «received»). Se il tuo hosting o il tuo firewall filtra gli indirizzi, invia la posizione nell’intestazione X-Upload-Offset anziché nell’indirizzo.

curl -X POST "https://misi.link/api/upload/chunk?id=aB3dE5fG&token=…&file=101&offset=0" \
  -H "Content-Type: application/octet-stream" --data-binary @morceau-1.bin
# {"ok": true, "received": 8388608, "done": false}

curl "https://misi.link/api/upload/status?id=aB3dE5fG&token=…"

3. Concludere

Quando tutti i file sono stati ricevuti, concludi: il trasferimento va online e le e-mail partono. Il link è quello della prima risposta («url»). Questo passaggio è obbligatorio. Poi il server archivia i file: nel frattempo «status» vale «finalizing». Un trasferimento ricevuto per intero ma mai concluso viene concluso automaticamente dopo 30 minuti di inattività.

curl -X POST https://misi.link/api/upload/complete -H "Content-Type: application/json" \
  -d '{"id": "aB3dE5fG", "token": "…"}'

Invio diretto nello spazio di archiviazione (consigliato per i file grandi)

Con «direct»: true, il tuo software invia il file direttamente nello spazio di archiviazione di misi.link, senza passare dal server: velocità massima e online in pochi secondi alla fine. Un solo file per trasferimento (più file: riuniscili in un archivio). Senza questa opzione, o se la risposta contiene «direct»: null, carica i pezzi con «chunk» come sopra.

  1. Crea il trasferimento con «direct»: true. La risposta indica «direct.part_size» (dimensione di ogni parte, l’ultima è più corta), «direct.parts» (numero di parti) e «direct.sign».
  2. Chiedi a «sign» gli indirizzi firmati (identificativo, token e numeri delle parti, al massimo 100 per chiamata; validi 6 ore), poi invia ogni parte con un semplice PUT al suo indirizzo, senza intestazione di autenticazione. Le parti possono partire in parallelo ed essere reinviate in caso di errore.
  3. Concludi con «complete». Se manca una parte, la risposta è «incomplete» con l’elenco «missing»: reinvia quelle parti e richiama «complete». Crittografia end-to-end: ogni parte è una sequenza di segmenti cifrati interi, in ordine (vedi l’esempio).
curl -X POST https://misi.link/api/v1/transfers -H "Authorization: Bearer $MISILINK_KEY" -H "Content-Type: application/json" \
  -d '{"files": [{"name": "film.mov", "size": 5368709120}], "direct": true}'
# {…, "direct": {"part_size": 16777216, "parts": 320, "total": 5368709120, "sign": "https://misi.link/api/upload/sign"}}

curl -X POST https://misi.link/api/upload/sign -H "Content-Type: application/json" \
  -d '{"id": "aB3dE5fG", "token": "…", "parts": [1, 2, 3]}'
# {"ok": true, "urls": {"1": "https://…", "2": "https://…", "3": "https://…"}}

curl -X PUT -H "Content-Type: application/octet-stream" --data-binary @part-1.bin "https://…"

curl -X POST https://misi.link/api/upload/complete -H "Content-Type: application/json" -d '{"id": "aB3dE5fG", "token": "…"}'
# {"ok": false, "error": "incomplete", "missing": [2]}

Crittografia end-to-end (facoltativa)

Il tuo software cifra il file prima di inviarlo: misi.link riceve e conserva solo dati illeggibili. La chiave non viene mai inviata al server: si trova nel link, dopo «#». Il destinatario scarica e decifra nel suo browser, sulla pagina abituale.

  1. Genera a caso una chiave di 16 byte, poi crea il trasferimento con «e2e»: true e un solo file (la sua dimensione in chiaro, in byte). Per più file, riuniscili prima in un archivio.
  2. Chiave di cifratura: HKDF-SHA-256 della chiave (sale «misilink-e2e-v1», info = identificativo del trasferimento, 16 byte), per AES-GCM.
  3. Dividi il file in segmenti di 4 MiB (4.194.304 byte). Segmento n. i: nonce = 4 byte nulli poi i su 8 byte (big-endian); dati associati = i su 8 byte poi 1 byte (1 per l’ultimo segmento, altrimenti 0). Ogni segmento cifrato ha 16 byte in più: carica questi byte con «chunk», in ordine («e2e.encrypted_size» in totale), poi concludi con «complete».
  4. Consigliato: «e2e_verify» = HKDF-SHA-256 della chiave (stesso sale, info «misilink-verify», 32 byte) in base64url. misi.link rifiuta così un download con una chiave sbagliata, senza mai conoscere quella giusta.
  5. Link da condividere: «url» + «#» + la chiave in base64url (22 caratteri). Con destinatari («recipients»), aggiungi «e2e_key» (la chiave): misi.link la conserva sigillata per inserirla nei link delle e-mail.
curl -X POST https://misi.link/api/v1/transfers \
  -H "Authorization: Bearer $MISILINK_KEY" -H "Content-Type: application/json" \
  -d '{"files": [{"name": "film.mov", "size": 5368709120}], "e2e": true, "e2e_verify": "…"}'
# {"ok": true, "id": "aB3dE5fG", …, "e2e": {"segment": 4194304, "tag": 16, "encrypted_size": 5368729600, "version": 1}}

Esempio completo in Python (libreria cryptography):

#!/usr/bin/env python3
# misi.link: end-to-end encrypted upload through the API (Python 3.8+, pip install cryptography)
# The file goes straight to the storage (signed URLs); if direct upload is unavailable, through "chunk".
# Usage: MISILINK_KEY=mlk_... python3 misilink_e2e.py file.mov [https://misi.link]
import base64, json, os, sys, urllib.request
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

SEG, SALT = 4 * 1024 * 1024, b'misilink-e2e-v1'
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b'=').decode()
hkdf = lambda ikm, info, n: HKDF(SHA256(), n, SALT, info).derive(ikm)


def call(url, body=None, raw=None, headers=None, method=None, auth=True):
    h = dict(headers or {})
    if auth:
        h['Authorization'] = 'Bearer ' + os.environ['MISILINK_KEY']
    data = raw if raw is not None else (json.dumps(body).encode() if body is not None else None)
    if data is not None:
        h.setdefault('Content-Type', 'application/octet-stream' if raw is not None else 'application/json')
    req = urllib.request.Request(url, data=data, headers=h, method=method or ('POST' if data is not None else 'GET'))
    with urllib.request.urlopen(req, timeout=600) as r:
        body = r.read()
        return json.loads(body) if body[:1] == b'{' else {}


def segments(path, key, size):
    n = max(1, -(-size // SEG))
    with open(path, 'rb') as f:
        for i in range(n):
            last = i == n - 1
            nonce = bytes(4) + i.to_bytes(8, 'big')
            aad = i.to_bytes(8, 'big') + (b'\x01' if last else b'\x00')
            yield key.encrypt(nonce, f.read(SEG), aad)        # SEG bytes in, SEG + 16 bytes out


def send(path, base='https://misi.link'):
    size = os.path.getsize(path)
    secret = os.urandom(16)                                   # the key: never sent to the server
    verify = b64(hkdf(secret, b'misilink-verify', 32))       # lets the server recognise the right key
    t = call(base + '/api/v1/transfers', {'files': [{'name': os.path.basename(path), 'size': size}],
                                         'e2e': True, 'e2e_verify': verify, 'direct': True})
    key = AESGCM(hkdf(secret, t['id'].encode(), 16))
    auth = {'id': t['id'], 'token': t['token']}
    d = t.get('direct')
    if d:                                                     # straight to the storage: parts of d['part_size'] bytes
        part, n, urls = b'', 0, {}
        def put(n, data):
            if str(n) not in urls:
                urls.update(call(d['sign'], {**auth, 'parts': list(range(n, min(n + 100, d['parts'] + 1)))})['urls'])
            call(urls[str(n)], raw=data, method='PUT', auth=False)
        for seg in segments(path, key, size):
            part += seg
            if len(part) == d['part_size']:
                n += 1; put(n, part); part = b''
        if part:
            n += 1; put(n, part)
    else:                                                     # through the server
        sent = 0
        for seg in segments(path, key, size):
            r = call(t['upload']['chunk'] + '?id=' + t['id'] + '&token=' + t['token'] + '&file=' + str(t['files'][0]['id']),
                     raw=seg, headers={'X-Upload-Offset': str(sent)})
            sent = r['received']
    call(t['upload']['complete'], auth)
    return t['url'] + '#' + b64(secret)                      # the key travels after "#", only in the link


if __name__ == '__main__':
    print(send(sys.argv[1], *(sys.argv[2:3] or [])))

Altre richieste

GET /api/v1/transfers
I tuoi trasferimenti, 50 per pagina (?page=2…).
GET /api/v1/transfers/{id}
Un trasferimento: stato, link, file, numero di download, data di scadenza.
DELETE /api/v1/transfers/{id}
Eliminare un trasferimento (il link non funziona più).
GET /api/v1/me
Il tuo account, il tuo piano e i suoi limiti, dimensione dei pezzi.

Date in formato ISO 8601 (UTC), dimensioni in byte.

Risposte ed errori

Tutte le risposte sono in JSON, con «ok». In caso di errore: «error» (codice breve) e «message». Codici HTTP: 401 chiave mancante o non valida, 403 piano senza API, 404 non trovato, 409 pezzo nel posto sbagliato, 422 richiesta rifiutata, 429 troppe richieste (attendi un minuto).